为什么我的代码这么慢?

时间:2011-08-18 16:32:49

标签: c# profiling extension-methods

我编写了以下扩展方法来从字典中获取元素or null if the key isn't present

public static TValue ItemOrNull<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
{
    try
    {
        return dict[key];
    }
    catch (KeyNotFoundException ex)
    {
        return default(TValue);
    }
}

我注意到我的程序运行速度非常慢,所以我使用高精度计时器类跟踪了这个扩展方法的问题。我得到了类似的结果〜连续100次:

DebugTimer.ResetTimer();
    dict1.ItemOrNull(key);
    dict2.ItemOrNull(key);
DebugTimer.StopTimer();

需要大约110,000,000个刻度(在我的处理器上超过0.03秒)。虽然更详细的版本:

DebugTimer.ResetTimer();
    if (dict1.ContainsKey(key))
        y = dict1[key];
    if (dict2.ContainsKey(key))
        z = dict2[key];
DebugTimer.StopTimer();
MessageBox.Show(y.ToString(), z.ToString()) // so the compiler doesn't optimize away y and z

需要大约6,000个小时(小于0.000002秒)。

为什么我的扩展方法版本比详细版本花费的时间长4个数量级,这是否清楚?

5 个答案:

答案 0 :(得分:15)

不要捕获流量控制的异常 - 它不仅仅会导致性能问题(尽管它们并不像大多数人想象的那么糟糕 - 正如Eric所说,大多数人对异常性能的恐惧来自于使用调试器)。它更多的是关于异常的逻辑性质。就个人而言,我不会以这种方式使用例外,即使它们基本上是免费的

这里发生了什么不好的事吗?对于用户要求此密钥值的任何方式都无效吗?绝对不是 - 该方法的全部目的是提供默认值。关键是缺席并不是特例 - 所以你应该寻找一种无异常的工作方式。

现在Dictionary<,>已经有了一种方法,可以让您在密钥存在时获取值,并让您知道是否找到了该值:TryGetValue

public static TValue GetValueOrDefault<TKey, TValue>(
    this IDictionary<TKey, TValue> dict,
    TKey key)
{
    TValue ret;
    // We don't care about the return value - we want default(TValue)
    // if it returns false anyway!
    dict.TryGetValue(key, out ret);
    return ret;
}

扩展方法只是编译成常规静态方法调用,因此它们没有性能差异。

您可能还想添加一个重载,允许用户在未找到密钥时表达要返回的默认值:

public static TValue GetValueOrDefault<TKey, TValue>(
    this IDictionary<TKey, TValue> dict,
    TKey key, TValue defaultValue)
{
    TValue ret;
    return dict.TryGetValue(key, out value) ? ret : defaultValue;
}

顺便提一下,我已调整方法的名称以匹配TryGetValue。显然你不必遵循它 - 这只是一个建议。

答案 1 :(得分:3)

这是因为您不在详细版本中使用异常。制作一个使用ContainsKey并进行比较的扩展方法。

澄清:异常可能非常缓慢。这是不依赖它们来控制正常工作流程的原因之一。它们适用于实际意外和不需要的情况。

对于FW3.5 +,请像其他人建议的那样使用TryGetValue

答案 2 :(得分:1)

这里有两个问题:

  1. 您的扩展方法未使用与测试相同的代码。如果你切换它来使用相同的代码(或者更好,Dictionary.TryGetValue),时间将会更加接近。
  2. 我怀疑你在调试模式下正在做你的计时。在Visual Studio 之外的完整版本构建中执行您的计时。调试模式(甚至是Release中的VS主机进程)的时序非常不准确,因为托管进程真的会减慢很多代码的速度。扩展方法实际上没有额外的开销(它们被编译为普通的静态方法调用) - 但是当在Visual Studio的调试器中运行时,方法调用通常会有夸大的开销,所以这看起来会比现实更糟。

答案 3 :(得分:0)

Dictionary是一个哈希表,因此Dictionary.Contains运行得很快(这里调用一步),它的成本很低,因为它直接检查内存中与键相关的值。 [键 - &GT;哈希然后检查内存]

但尝试部分的第一个功能已经检查过了!在返回之前,对值键进行哈希处理并检查内存。不久,它已经包含了Dictionary.Contains的步骤。

你的代码运行的原因是“try”块的速度慢,其中包括具有缺失bool值的Dictionary.Contains的步骤,导致陷入catch块。这浪费了你的时间。上面的答案是正确的,更详细的。

答案 4 :(得分:0)

你只需要将时间值添加到我的教授声明中,“异常处理是一个代价高昂的过程!不要在Catch块中编写所有逻辑,尝试编写代码使得你有可以避免异常的检查。”使用异常处理来处理意外异常,而不是明显当项目不在字典中时会崩溃!