【问题标题】:What is the overhead for a method call in a loop?循环中方法调用的开销是多少?
【发布时间】:2012-10-30 09:10:12
【问题描述】:

我一直在研究 C# 迷宫生成器,它可以生成 128000x128000 像素的迷宫。所有内存使用都已优化,因此我目前正在考虑加快生成速度。

我发现的一个问题(远远超出兴趣点)如下(只是一些示例代码来说明问题):

当 pixelChanged 为 null 时,此代码在我的机器上运行大约 1.4 秒:

public void Go()
{
    for (int i = 0; i < bitjes.Length; i++)
    {
        BitArray curArray = bitjes[i];
        for (int y = 0; y < curArray.Length; y++)
        {
            curArray[y] = !curArray[y];
            GoDrawPixel(i, y, false);
        }
    }
}

public void GoDrawPixel(int i, int y, Boolean enabled)
{
    if (pixelChanged != null)
    {
        pixelChanged.Invoke(new PixelChangedEventArgs(i, y, enabled));
    }
}

以下代码的运行速度实际上快了 0.4 秒

public void Go()
{
    for (int i = 0; i < bitjes.Length; i++)
    {
        BitArray curArray = bitjes[i];
        for (int y = 0; y < curArray.Length; y++)
        {
            curArray[y] = !curArray[y];
            if (pixelChanged != null)
            {
                pixelChanged.Invoke(new PixelChangedEventArgs(i, y, false));
            }
        }
    }
}

似乎当只调用一个“空”方法时,该算法使用了大约 20% 的 cpu。这不是很奇怪吗?我尝试在调试和发布模式下编译解决方案,但没有发现任何明显的差异。

这意味着我在这个循环中的每个方法调用都会使我的代码减慢大约 0.4 秒。由于迷宫生成器代码目前包含许多执行不同操作的单独方法调用,因此这开始获得大量。

我还查看了 Google 和 Stack Overflow 上的其他帖子,但还没有真正找到解决方案。

是否可以像这样自动优化代码? (也许像 Roslyn 项目这样的东西???)或者我应该把所有东西放在一个大方法中?

编辑: 我也有兴趣对这两种情况下的 JIT/CLR 代码差异进行一些分析。 (所以这个问题实际上来自哪里)

编辑2: 所有代码均在发布模式下编译

【问题讨论】:

    标签: c# performance compiler-construction


    【解决方案1】:

    这是一个问题,JIT 对方法进行了内联优化(其中整个方法代码实际上被注入到调用父代码中)但这只发生在编译为 32 字节或更少字节的方法上。我不知道为什么存在 32 字节限制,并且还希望在 C# 中看到一个“内联”关键字,就像在 C/C++ 中一样,正是针对这些问题。

    【讨论】:

    • 我从来没有听说过 inline 关键字,但它似乎完全符合我的需要:),我希望微软有一天也可以将它添加到 C# 中。
    • 我尝试将 [MethodImpl(MethodImplOptions.AggressiveInlining)] 放在我的 GoDrawPixel(......) 方法前面,但无济于事。它仍然比直接实现慢 0.4 秒。
    • 我正在使用它,但它没有任何区别:/
    • 我希望有人能帮我解决这个问题,因为如果可行的话,这似乎是最有希望的答案。
    • 您是否将编译设置为“发布”?如果你知道这一点,把它写在你的问题中,因为这也让我感兴趣。
    【解决方案2】:

    我会尝试的第一件事是将其设为静态而不是实例:

    public static void GoDrawPixel(PixelChangedEventHandler pixelChanged,
        int x, int y, bool enabled)
    {
        if (pixelChanged != null)
        {
            pixelChanged.Invoke(new PixelChangedEventArgs(x, y, enabled));
        }
    }
    

    这改变了一些事情:

    • 堆栈语义保持可比性(它加载一个引用、2 个整数和一个布尔值)
    • callvirt 变为 call - 这避免了一些小开销
    • ldarg0/ldfld 对 (this.pixelChanged) 变为单个 ldarg0

    我接下来要看的是PixelChangedEventArgs;如果避免大量分配,可能将其作为结构传递会更便宜;或者只是:

    pixelChanged(x, y, enabled);
    

    (原始参数而不是包装对象 - 需要更改签名)

    【讨论】:

    • 我已经研究了 pixelChanged(x, y, enabled) 确实加快了速度。您能否解释一下 callvirt 和 call 差异的含义?还有 ldarg0/ldfld 对?
    • @Devedse 在实例方法中,你访问this.pixelChanged 两次 where-如果我们只执行一次,就好像我们在一个参数中传递它一样;一旦引用作为本地或参数(在本例中为 arg0)在堆栈上,它会略微更快地获取;此外,如果我们将其设为静态而不是实例,则必须将其设为参数。
    • 我试过你的代码,现在它慢了大约 0.25 秒而不是 0.4 秒。我们正在改进:)
    【解决方案3】:

    这是处于调试模式还是发布模式?方法调用相当昂贵,但是当您在发布模式下构建/运行它时,它们可能会被内联。在调试模式下,它不会从编译器获得任何优化。

    【讨论】:

    • 我在发布和调试中都试过了。在调用方法的情况下,两者都慢了大约 0.4 秒。
    • @Devedse:你是如何内联方法的?
    • @Luis Filipe:我手动复制了代码,正如您在主题中看到的那样。
    【解决方案4】:

    正如 Marc 所说,主要开销是进行虚拟调用并传递参数。 PixelChanged 的​​值可以在方法执行过程中改变吗?如果不是,这可能有效(我不完全确定 JIT 将空操作委托优化为 nop,您必须自己测试它(如果不是,我将忽略这里的良好实践,只做调用,一个调用 pixelChanged.Invoke,一个不调用(内联),只调用最适合的调用...毕竟有时您必须使代码不那么优雅以使其更快)。

    public void Go()
    {
      if (pixelChanged != null)  
         GoPixelGo((x,y,z) => { });  
      else
         GoPixelGo((i, y, enabled) => pixelChanged.Invoke(i, y, enabled));
    }
    
    public void GoPixelGo(Action<int, int, bool> action)
    {
      for (int i = 0; i < bitjes.Length; i++)
      {
          BitArray curArray = bitjes[i];
          for (int y = 0; y < curArray.Length; y++)
          {
             curArray[y] = !curArray[y];
             action(i,y, false);
          }
      }
    }
    

    【讨论】:

    • 我花了一段时间才理解,但我确实看到了你现在在做什么。我也试试这个。
    • 我想这取决于您需要针对哪种情况进行优化。对于“pixelChanged is null”的情况,由于委托调用的成本(快,但不如空测试快),我希望空测试更快。对于“pixelChanged not null”的情况,确实删除空测试可能会有所帮助。这真的取决于哪种情况最有可能。
    • 我已经尝试过了,它也比直接方式慢了大约 0.25 秒。
    猜你喜欢
    • 2011-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-26
    • 1970-01-01
    • 2012-12-08
    • 2011-07-25
    • 2011-05-17
    相关资源
    最近更新 更多