使用委托会影响性能吗?过度使用委托是个坏主意吗?

5

考虑以下代码:

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

GetDetailedDebugInfo()可能是一个开销较大的方法,因此我们只想在运行于调试模式下时才调用它。

现在,更为简洁的选择是编写如下代码:

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

在定义了.Log()的地方:

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

我的关注点是性能,初步测试并未显示第二种情况特别昂贵,但如果负载增加,我不想遇到任何意外。
哦,而且请不要建议条件编译,因为它不适用于这种情况。
(附言:我直接在StackOverflow的“提问”文本区域中编写了代码,如果有微妙的错误并且无法编译,请不要责怪我,你明白我的意思 :)

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

5
不,它不应该表现糟糕。毕竟,您只会在调试模式下调用它,性能并不是最重要的。实际上,您可以删除lambda并只传递方法名称,以消除不必要的中间匿名方法的开销。
请注意,如果您想在Debug构建中执行此操作,可以将[Conditional("DEBUG")]属性添加到日志记录方法中。

那么,Lambda表达式无论是否被调用,都会捕获变量,因此强制将它们从堆栈移动到堆上。这实际上会对性能产生相当大的影响,因为JIT更加谨慎地缓存字段值在寄存器中,所以在捕获变量时很可能会执行额外的内存加载和存储操作 - 在紧密循环中您真的会注意到差异。 - Pavel Minaev
1
@Pavel:一般来说,这是正确的。但对于OP的目的,没有捕获变量。 - Mehrdad Afshari
我提到条件编译不适用于这种情况。我写的样例代码只是一个简单的测试案例,我的生产代码与日志记录和调试模式完全无关 - 但应该以类似的方式工作。 - andreialecu
@Mehrad:这并不适用,因为我不一定只在调试模式下调用它。请忽略我的小样例所暗示的内容。在实际应用中,条件可能不是“IsInDebugMode”,而是完全不同的东西,这取决于多种因素 - 可能现在为真,在下一次调用时为假。 - andreialecu
+1 是为了提醒大家不要低估 [Conditional("DEBUG")] 的作用。 - Vlad
显示剩余2条评论

3

性能上有所差异。它的显著程度取决于您代码的其余部分,因此我建议在进行优化之前进行性能分析。

话虽如此,在您的第一个示例中:

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毫秒。

看起来我自己解决了我的问题。


1

你也可以这样做:

// 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)
{
    ....
}

0
直接调用 getMessage 委托而不是在其上调用 Invoke。
if(IsDebuggingEnabled)
{
  LogInternal(getMessage());
}

你还应该在 getMessage 上添加 null 检查。


我的初始评论包含了一个错误的陈述,即直接调用委托而不是在其上调用Invoke会更快。然而,我错了,我已经删除了那个评论。我编写了一个小的控制台应用程序,在其中通过直接调用和调用Invoke来调用类型为Func<string>的委托。然后,我使用ildasm工具查看生成的IL代码,对于这两个调用,IL代码是相同的: callvirt instance !0 class[System.Core]System.Func'1<string>::Invoke() - Mehmet Aras

0

标准答案:

  • 如果你必须做它,那就去做吧。
  • 循环10^9次,看秒表,这会告诉你需要多少纳秒。
  • 如果你的程序很大,那么很可能你在其他方面也有更大的问题。

-3

我相信委托会创建一个新的线程,所以你关于它提高性能的说法可能是正确的。为什么不像Dav建议的那样设置一个测试运行,并密切关注应用程序生成的线程数量,你可以使用Process Explorer进行监控。

等等!我被纠正了!只有当你使用“BeginInvoke”时,委托才会使用线程...所以我的上述评论并不适用于你使用委托的方式。


10
你的信念应该重新考虑。代理(Delegates)与线程(Threads)完全无关。 - Mehrdad Afshari
3
委托可以用于轻松执行一些多线程操作,这可能是造成混淆的原因。 - Rex M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接