Why is it faster to check if dictionary contains the key, rather than catch the exception in case it doesn't?
想象一下代码:
1 2 3 4 5 6
| public class obj
{
// elided
}
public static Dictionary <string, obj > dict = new Dictionary <string, obj >(); |
方法1
1 2 3 4 5 6 7 8
| public static obj FromDict1(string name)
{
if (dict.ContainsKey(name))
{
return dict[name];
}
return null;
} |
号
方法2
1 2 3 4 5 6 7 8 9 10 11
| public static obj FromDict2(string name)
{
try
{
return dict[name];
}
catch (KeyNotFoundException)
{
return null;
}
} |
我很好奇这两个函数在性能上是否存在差异,因为第一个函数应该比第二个函数慢,因为它需要检查两次字典是否包含值,而第二个函数只需要访问一次字典,但哇,实际上是相反的:
循环1000 000个值(现有100 000个,不存在90 000个):
first function: 306 milliseconds
second function: 20483 milliseconds
号
为什么?
编辑:正如您在下面这个问题的注释中所注意到的,如果有0个不存在的键,第二个函数的性能实际上比第一个函数稍好。但是,一旦存在至少一个或多个不存在的密钥,第二个密钥的性能就会迅速下降。
- 为什么第一个要慢一点?事实上,乍一看,我认为应该更快些,预计ContainsKey会是O(1)的…
- msdn.microsoft.com/en-us/library/vstudio/&hellip;
- 见stackoverflow.com/a/52390/759019
- @trustme-i'madoctor是因为即使它是O(1),它也需要一些指令,而第二个函数根本不调用containskey(我认为访问dict一次比访问它两次快)。
- @与字典中的O(1)查找相比,Petr在异常抛出中涉及的指令更多…尤其是在两个O(1)操作之后,O(1)操作仍然是渐进的。
- 如果没有附加调试程序,它是否仍然很慢?我以前发现,仅仅附加一个调试器就可以显著地降低异常代码的速度。
- 正如下面的好答案所指出的,抛出异常是昂贵的。他们的名字表明了这一点:他们是为特殊情况而保留的。如果你正在运行一个循环,在这个循环中你查询字典上一百万次不存在的键,那么它就不再是一种特殊情况了。如果您在查询字典中的键,并且它们的键不存在是一个相对常见的情况,那么首先检查是有意义的。
- @Jonathan这个测试是在关闭调试器的情况下完成的
- 别忘了,与抛出一百万个异常相比,您只比较了检查一百万个缺失值的成本。但这两种方法在访问现有值的成本上也有所不同。如果缺少的键足够少,那么异常方法将更快,尽管在缺少键时它的成本更高。
- +1用代码和度量来支持您的问题!
一方面,抛出异常本身就很昂贵,因为堆栈必须解除绑定等。另一方面,通过字典的键访问一个值是便宜的,因为它是一个快速的O(1)操作。
顺便说一句:正确的方法是使用TryGetValue。
1 2 3 4
| obj item;
if(!dict.TryGetValue(name, out item))
return null;
return item; |
这只访问字典一次而不是两次。如果您真的想返回null,如果密钥不存在,可以进一步简化上述代码:
1 2 3
| obj item;
dict.TryGetValue(name, out item);
return item; |
号
这是有效的,因为如果不存在带name的密钥,TryGetValue将item设置为null。
- 我根据答案更新了我的测试,出于某种原因,尽管建议的功能更快,但实际上并不重要:原始的264毫秒,建议的258毫秒
- @佩特:是的,这并不重要,因为查字典的速度很快,你做一次或两次都没关系。这些250毫秒中的大部分很可能是在测试循环本身中花费的。
- 这是很好的了解,因为有时人们会觉得异常抛出是处理诸如不存在的文件或空指针之类的情况的更好或更干净的方法,不管这些情况是否常见,也不考虑性能成本。
- @拉尔斯,这也取决于你在做什么。当循环开始时,像这样的简单微基准显示了对异常的巨大惩罚,包括在每次迭代中抛出异常的文件或数据库活动,对性能影响很小。比较第一和第二个表:codeproject.com/articles/11265/&hellip;
- 老实说,我想知道你为什么要用!xxx.TryGetValue... return null而不是xxx.TryGetValue... return item,在我看来,返回空值应该是方法中的最后一个操作,如果找到了什么,只返回项。当然,如果你有一个更长的方法,并且你正在(试图)减少缩进,这会有所不同。
- @我不明白你在说什么。为什么return null应该是方法中的最后一个操作?这会导致严重的意图。最好换一种方式来做:只要条件之一不成立,就返回空值。但是在这个具体的例子中,假设这个方法只是您在这里看到的,它实际上只是一个偏好问题。你用哪种方式做都没什么区别。
- 查找为o(1)完全不表示只需时间不受元素数量影响就可以了。
- @runefs:没错。这就是为什么我的答案说这是一个快速的,O(1)操作,即它是快速的,它保持这样的方式,无论有多少元素。
- @Larsh也注意到,当试图访问一个文件(或其他外部资源)时,它可能会在检查和实际访问尝试之间改变状态。在这些情况下,使用异常是正确的方法。有关更多信息,请参阅Stephen C对此问题的回答。
- @Yoniyalovitsky:很好的观点。感谢您添加。
- 老实说,我误读为"因为它是O(1)",我同意在大多数情况下,它是正确的,因为它是快速的,但挑剔它是不保证的,因为我们不知道gethashkey的实现
- @runefs:是的,这在技术上是正确的,是的,这是吹毛求疵的。—)根据合同,GetHashCode的实施需要很快。
字典专门设计用于执行超快速键查找。它们被实现为哈希表,并且条目越多,它们相对于其他方法就越快。只有当您的方法未能完成您设计它要做的事情时,才应该使用异常引擎,因为它是一个大的对象集,为您提供了许多处理错误的功能。我曾经构建了一个完整的库类,其中的所有内容都被try-catch块包围过一次,看到包含600多个异常中的每一个单独行的调试输出时,我感到非常震惊!
- 当语言实现者决定在哪里进行优化时,哈希表将获得优先权,因为它们经常被使用,通常在可能成为瓶颈的内部循环中。预期例外情况的使用频率要低得多,在不寻常的情况下("例外",可以说),因此通常不认为它们对性能很重要。
- "它们是作为哈希表实现的,并且与其他方法相比,条目越多,实现速度就越快。"如果存储桶满了,这当然不是真的吗????!
- @Anthonylambert想说的是,搜索哈希表具有O(1)时间复杂性,而二元搜索树搜索具有O(log(n));树会随着元素数量的渐进增加而减慢,而哈希表则没有。因此,哈希表的速度优势随着元素数量的增加而增加,尽管它做得很慢。
- @在正常使用的情况下,字典的哈希表中几乎没有冲突。如果您使用的是哈希表,而您的存储桶已满,那么您有太多的条目(或存储桶太少)。在这种情况下,是时候使用自定义哈希表了。