为啥检查字典是不是包含键会更快,而不是捕获异常以防万一?

Posted

技术标签:

【中文标题】为啥检查字典是不是包含键会更快,而不是捕获异常以防万一?【英文标题】:Why is it faster to check if dictionary contains the key, rather than catch the exception in case it doesn't?为什么检查字典是否包含键会更快,而不是捕获异常以防万一? 【发布时间】:2013-04-12 16:19:22 【问题描述】:

想象一下代码:

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;

方法二

public static obj FromDict2(string name)

    try
    
        return dict[name];
    
    catch (KeyNotFoundException)
    
        return null;
    

我很好奇这两个函数的性能是否存在差异,因为第一个应该比第二个慢 - 因为它需要检查字典是否包含一个值,而第二个函数确实需要访问字典只有一次,但哇,它实际上是相反的:

循环获取 1 000 000 个值(100 000 个现有值和 900 000 个不存在值):

第一个函数:306 毫秒

秒函数:20483毫秒

为什么会这样?

编辑:正如您在这个问题下面的 cmets 中注意到的那样,如果有 0 个不存在的键,第二个函数的性能实际上比第一个函数略好。但是一旦有至少 1 个或多个不存在的 key,第二个 key 的性能就会迅速下降。

【问题讨论】:

为什么第一个应该慢一些?实际上,乍一看,我会说它应该更快,ContainsKey 预计 O(1) ... msdn.microsoft.com/en-us/library/vstudio/… @Petr 与O(1) 在字典中查找相比,抛出异常涉及的指令要多得多...尤其是因为执行两个O(1) 操作仍然是渐近的O(1) 正如在下面的好答案中指出的那样,抛出异常是昂贵的。它们的名字暗示了这一点:它们是为例外-al 情况保留的。如果您正在运行一个循环,在其中查询字典一百万次以查找不存在的键,那么它就不再是一种例外情况。如果您要在字典中查询键,并且它们的键不存在是一种相对常见的情况,那么首先检查是有意义的。 不要忘记,您只比较了检查一百万个缺失值与抛出一百万个异常的成本。但是这两种方法在访问现有值的成本上也不同。如果缺少键的情况足够少,则异常方法总体上会更快,尽管 键不存在时它的成本更高。 【参考方案1】:

一方面,throwing exceptions is inherently expensive,因为堆栈必须展开等。 另一方面,通过键访问字典中的值很便宜,因为它是一个快速的 O(1) 操作。

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

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

这只会访问字典一次,而不是两次。 如果你真的想在key不存在的情况下只返回null,上面的代码可以进一步简化:

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

这是可行的,因为如果不存在带有 name 的键,TryGetValue 会将 item 设置为 null

【讨论】:

我根据答案更新了我的测试,出于某种原因,尽管建议的功能更快,但实际上并不是很重要:原始 264 毫秒,建议 258 毫秒 @Petr:是的,这并不重要,因为访问字典非常快,执行一次或两次并不重要。这 250 毫秒的大部分时间很可能用于测试循环本身。 这很好知道,因为有时人们会觉得抛出异常是一种更好或更简洁的方式来处理诸如不存在文件或空指针之类的情况,无论这些情况是否常见,并且不考虑性能成本。 @LarsH 这也取决于你在做什么。虽然像这样的简单微基准测试表明,一旦您的循环开始包括文件或数据库活动,在每次迭代中抛出异常,就会对异常产生非常大的惩罚,这对性能影响很小。比较第一个和第二个表:codeproject.com/Articles/11265/… @LarsH 另请注意,当尝试访问文件(或其他一些外部资源)时,它可能会在检查和实际访问尝试之间改变状态。在这些情况下,使用异常是正确的方法。请参阅Stephen C's answer to this question 了解更多信息。【参考方案2】:

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

【讨论】:

当语言实现者决定在哪里进行优化时,哈希表将获得优先考虑,因为它们被频繁使用,通常在可能成为瓶颈的内部循环中。在异常(可以说是“异常”)情况下,预计异常的使用频率要低得多,因此它们通常不被认为对性能很重要。 “它们以哈希表的形式实现,相对于其他方法,条目越多,速度越快。”如果桶装满了,那肯定不是真的?!?! @AnthonyLambert 他想说的是搜索哈希表的时间复杂度为 O(1),而二叉搜索树搜索的时间复杂度为 O(log(n));随着元素数量渐近增加,树会变慢,而哈希表则不会。因此,哈希表的速度优势会随着元素数量的增加而增加,尽管它的速度会很慢。 @AnthonyLambert 在正常使用情况下,字典的哈希表中的冲突极少。如果您使用的是哈希表并且您的存储桶已满,那么您的条目太多(或存储桶太少)。在这种情况下,是时候使用自定义哈希表了。

以上是关于为啥检查字典是不是包含键会更快,而不是捕获异常以防万一?的主要内容,如果未能解决你的问题,请参考以下文章

当 catch 语句没有显式捕获异常对象时,它们是不是更快/更便宜? [关闭]

Javascript:检查正则表达式是不是包含捕获组而不执行它[关闭]

为啥在 MergeSort 中使用 InsertionSort 而不是 Merge 平均更快?

在 Java 中对资源使用 try 是不是安全 - 它是不是检查可关闭对象是不是不为空,是不是在尝试关闭它时捕获异常

为啥不抛出异常的代码允许捕获已检查的异常?

为啥从标准模块(而不是用户窗体)调用 VBA 代码时运行得更快?