为什么检查字典是否包含键而不是捕获异常(如果没有包含键)会更快呢?


234

想象一下代码:

public class obj
{
    // elided
}

public static Dictionary<string, obj> dict = new Dictionary<string, obj>();

方法一

public static obj FromDict1(string name)
{
    if (dict.ContainsKey(name))
    {
        return dict[name];
    }
    return null;
}

方法2

public static obj FromDict2(string name)
{
    try
    {
        return dict[name];
    }
    catch (KeyNotFoundException)
    {
        return null;
    }
}

我很好奇这两个函数的性能是否有所不同,因为第一个函数的速度应该比第二个函数的慢-鉴于它需要检查两次字典是否包含值,而第二个函数确实只需要访问字典除了WOW,它实际上是相反的:

循环1000000个值(现有10万个和不存在90万个):

第一个功能:306毫秒

第二功能:20483毫秒

这是为什么?

编辑:正如您可以在此问题下方的注释中注意到的那样,在有0个不存在的键的情况下,第二个函数的性能实际上比第一个更好。但是,一旦至少有一个或多个不存在的密钥,第二个密钥的性能就会迅速下降。


39
为什么第一个应该慢一些?事实上,在乍看之下,我会说这应该会更快,ContainsKey预计O(1)...
PatrykĆwiek


8
@Petr O(1)与字典中的查找相比,引发异常时涉及的指令要多得多……尤其是因为执行两个O(1)操作仍是渐近的O(1)
PatrykĆwiek2013年

9
正如下面的好答案中指出的那样,抛出异常的代价很高。他们的名字表明了这一点:它们是为例外情况保留的。如果您正在运行一个循环,在该循环中,您查询一百万次字典以查找不存在的键,那么这种情况就不再是一种特殊情况了。如果您要在字典中查询密钥,并且相对常见的情况是它们不存在密钥,那么首先检查是有意义的。
Jason R

6
不要忘记,您只比较了检查一百万个缺失值的成本和抛出一百万个异常的成本。但是,这两种方法在获取现有价值的成本上也有所不同。如果缺少密钥非常少见,则异常方法总体上将更快,尽管缺少密钥代价更高。
Alexis

Answers:


404

一方面,抛出异常本质上是昂贵的,因为必须解开堆栈等。
另一方面,通过字典的键来访问字典中的值很便宜,因为它是一种快速的O(1)操作。

顺便说一句:正确的方法是使用 TryGetValue

obj item;
if(!dict.TryGetValue(name, out item))
    return null;
return item;

这仅一次访问字典,而不是两次。
如果您确实只想返回null不存在的密钥,则可以进一步简化上述代码:

obj item;
dict.TryGetValue(name, out item);
return item;

之所以有效,是因为TryGetValue设置item为,null如果不name存在。


4
我根据答案更新了测试,由于某种原因,尽管建议的功能运行速度更快,但实际上并不是很重要:264毫秒原始,258毫秒建议一个
Petr

52
@Petr:是的,这并不重要,因为访问字典非常快,一次或两次都没关系。这250毫秒中的大部分最有可能花费在测试循环本身中。
Daniel Hilgarth

4
这是个好消息,因为有时会给人一种印象,即抛出异常是一种处理不存在的文件或空指针之类的情况的更好或更干净的方法,而不管这些情况是否常见,而无需考虑性能成本。
LarsH 2013年

4
@LarsH,这也取决于您在做什么。尽管像这样的简单微基准测试对循环的惩罚非常大,但是一旦循环开始包括文件或数据库活动,则每次迭代都会引发异常,因此对性能的影响很小。:比较第一和第二台 codeproject.com/Articles/11265/...
丹摆弄心火

8
@LarsH还应注意,当尝试访问文件(或其他外部资源)时,它可能会在检查和实际访问尝试之间更改状态。在这些情况下,使用异常是正确的方法。有关其他信息,请参见Stephen C对这个问题的回答
yoniLavi

6

字典是专门为执行超快速键查找而设计的。它们被实现为哈希表,并且条目越多,它们相对于其他方法的速度就越快。仅当您的方法无法完成您设计的工作时,才应使用异常引擎,因为异常对象是大量的对象,为您提供了许多处理错误的功能。我一次构建了一个完整的库类,所有内容一次都被try catch块包围,并且震惊地看到调试输出,其中包含600多个异常中的每一个的单独一行!


1
当语言实现者决定将精力花在优化上的地方时,哈希表将获得优先级,因为它们经常被使用,并且经常在可能成为瓶颈的内部循环中使用。在异常(可以说是“例外”)情况下,期望异常的使用频率要低得多,因此通常不认为它们对性能很重要。
Barmar

“它们被实现为哈希表,并且条目越多,它们相对于其他方法的速度就越快。” 如果水桶装满了,那肯定是不正确的!!?!
AnthonyLambert

1
@AnthonyLambert他想说的是,搜索散列表的时间复杂度为O(1),而二叉搜索树搜索的复杂度为O(log(n)); 随着元素数量的渐近增加,树变慢,而哈希表却没有。因此,哈希表的速度优势会随着元素数量的增加而增加,尽管它的增长速度很慢。
2013年

@AnthonyLambert在正常使用下,字典的哈希表中几乎没有冲突。如果您使用的是哈希表,并且存储桶已满,那么您的条目太多(或存储桶太少)。在这种情况下,该使用自定义哈希表了。
AndrewS 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.