【问题标题】:Is using delegates excessively a bad idea for performance? [duplicate]过度使用代表对性能来说是个坏主意吗? [复制]
【发布时间】:2009-08-13 00:33:37
【问题描述】:

考虑以下代码:

if (IsDebuggingEnabled) { 
   instance.Log(GetDetailedDebugInfo()); 
}

GetDetailedDebugInfo() 可能是一个昂贵的方法,所以我们只想在调试模式下调用它。

现在,更简洁的替代方法是编写如下代码:

instance.Log(() => GetDetailedDebugInfo());

.Log() 的定义如:

public void Log(Func<string> getMessage)
{
    if (IsDebuggingEnabled) 
    {
        LogInternal(getMessage.Invoke());
    }
}

我关心的是性能,初步测试并没有显示第二种情况特别昂贵,但如果负载增加,我不想遇到任何意外。

哦,请不要建议条件编译,因为它不适用于这种情况。

(P.S.:我直接在 StackOverflow Ask a Question textarea 中编写了代码,所以如果有细微的错误并且无法编译,请不要怪我,你明白了 :)

【问题讨论】:

  • 很容易模拟增加的负载;您可以设置一个测试用例,在紧密循环中运行这两个选项,进行数万次迭代并进行比较。

标签: c# .net performance delegates lambda


【解决方案1】:

不,它不应该有一个糟糕的表现。毕竟,您只会在性能不在最前沿的调试模式下调用它。实际上,您可以移除 lambda 并传递方法名称,以移除不必要的中间匿名方法的开销。

请注意,如果您想在 Debug 构建中执行此操作,可以在 log 方法中添加 [Conditional("DEBUG")] 属性。

【讨论】:

  • 呃,无论是否调用 lambda,都会捕获变量并因此强制将它们从堆栈提升到堆上这一事实呢?这实际上会对性能产生相当大的影响,因为 JIT 在寄存器中缓存字段值的变化要大得多,所以当变量被捕获时,它可能会进行额外的内存加载和存储 - 你会真正注意到紧密循环中的差异。
  • @Pavel:总的来说,这是真的。但是对于 OP 的目的,没有捕获的变量。
  • 我提到条件编译不适用于这种情况。我作为示例编写的代码只是一个简单的测试用例,我的生产代码与日志记录和调试模式完全无关——但应该以类似的方式工作。
  • @legenden:为了完整起见,我提到了这一点。答案的第一段无论如何都适用。
  • @Mehrad:它并不真正适用,因为我不一定会在调试模式下调用它。请抽象一下我的小样本所暗示的内容。在实际应用程序中,条件可能不是“IsInDebugMode”,而是完全不同的情况,这可能取决于多种因素 - 现在可以为 True,下一次调用时为 False。
【解决方案2】:

性能存在差异。它的重要性取决于您的其余代码,因此我建议在开始优化之前进行分析。

对于你的第一个例子来说:

if (IsDebuggingEnabled) 
{ 
    instance.Log(GetDetailedDebugInfo()); 
}

如果 IsDebuggingEnabled 是静态只读的,那么检查将被忽略,因为它知道它永远不会改变。这意味着如果 IsDebuggingEnabled 为 false,上述示例对性能的影响为零,因为在 JIT 完成后,代码将消失。

instance.Log(() => GetDetailedDebugInfo());

public void Log(Func<string> getMessage)
{
    if (IsDebuggingEnabled) 
    {
        LogInternal(getMessage.Invoke());
    }
}

每次调用 instance.Log 时都会调用该方法。哪个会慢一些。

但在花时间进行这种微优化之前,您应该分析您的应用程序或运行一些性能测试,以确保这实际上是您应用程序的瓶颈。

【讨论】:

    【解决方案3】:

    我希望有一些关于这种情况下性能的文档,但似乎我得到的只是关于如何改进我的代码的建议......似乎没有人阅读我的 P.S. - 你没有积分。

    于是我写了一个简单的测试用例:

        public static bool IsDebuggingEnabled { get; set; }
    
    
        static void Main(string[] args)
        {
            for (int j = 0; j <= 10; j++)
            {
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i <= 15000; i++)
                {
                    Log(GetDebugMessage);
                    if (i % 1000 == 0) IsDebuggingEnabled = !IsDebuggingEnabled;
                }
                sw.Stop();
                Console.WriteLine(sw.ElapsedMilliseconds);
            }
    
            Console.ReadLine();
            for (int j = 0; j <= 10; j++)
            {
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i <= 15000; i++)
                {
                    if (IsDebuggingEnabled) GetDebugMessage();
                    if (i % 1000 == 0) IsDebuggingEnabled = !IsDebuggingEnabled;
                }
                sw.Stop();
                Console.WriteLine(sw.ElapsedMilliseconds);
            }
            Console.ReadLine();
        }
    
        public static string GetDebugMessage()
        {
            StringBuilder sb = new StringBuilder(100);
            Random rnd = new Random();
            for (int i = 0; i < 100; i++)
            {
                sb.Append(rnd.Next(100, 150));
            }
            return sb.ToString();
        }
    
        public static void Log(Func<string> getMessage)
        {
            if (IsDebuggingEnabled)
            {
                getMessage();
            }
        }
    

    两个版本的时间似乎完全相同。 我在第一种情况下得到 145 毫秒,在第二种情况下得到 145 毫秒

    看起来我回答了我自己的问题。

    【讨论】:

      【解决方案4】:

      您也可以这样做:

      // no need for a lambda
      instance.Log(GetDetailedDebugInfo)
      
      // Using these instance methods on the logger
      public void Log(Func<string> detailsProvider)
      {
          if (!DebuggingEnabled)
              return;
      
          this.LogImpl(detailsProvider());
      }
      
      public void Log(string message)
      {
          if (!DebuggingEnabled)
              return;
      
          this.LogImpl(message);
      }
      
      protected virtual void LogImpl(string message)
      {
          ....
      }
      

      【讨论】:

        【解决方案5】:

        标准答案:

        • 如果你必须这样做,你就必须这样做。
        • 循环 10^9 次,看一下秒表,它会告诉您需要多少纳秒。
        • 如果您的程序很大,您可能在其他地方遇到更大的问题。

        【讨论】:

          【解决方案6】:

          直接调用 getMessage 委托而不是对其调用 Invoke。

          if(IsDebuggingEnabled)
          {
            LogInternal(getMessage());
          }
          

          您还应该在 getMessage 上添加空检查。

          【讨论】:

          • 我最初的评论包括一个错误的陈述,说直接调用委托而不是调用它会更快。但是,我错了,我删除了该评论。我编写了一个小型控制台应用程序,在其中我通过直接调用和调用 Invoke 来调用 Func 类型的委托。然后我用 ildasm 工具查看了生成的 IL,两个调用的 IL 相同: callvirt instance !0 class[System.Core]System.Func'1::Invoke()
          【解决方案7】:

          我相信委托会创建一个新线程,因此您对它提高性能的看法可能是正确的。 为什么不像 Dav 建议的那样设置测试运行,并密切关注您的应用产生的线程数,您可以使用 Process Explorer。

          等一下!我已经纠正了!代表仅在您使用“BeginInvoke”时使用线程......所以我上面的 cmets 不适用于您使用它们的方式。

          【讨论】:

          • 你的信念应该被重新考虑。委托与线程完全无关。
          • 委托可以用来轻松做一些多线程操作。也许这就是混乱的来源。
          猜你喜欢
          • 2010-11-27
          • 1970-01-01
          • 2014-08-23
          • 1970-01-01
          • 1970-01-01
          • 2011-08-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多