循环中方法调用的开销是多少?

时间:2012-10-30 09:10:12

标签: c# performance compiler-construction

我一直在研究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));
            }
        }
    }
}

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

这意味着我在此循环中进行的每个方法调用都会使代码减慢约0.4秒。由于迷宫生成器代码当前包含许多单独的方法调用,这些调用超出了不同的操作,因此开始获得实质性的安装。

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

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

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

EDIT2: 所有代码均以发布模式编译

4 个答案:

答案 0 :(得分:5)

我要尝试的第一件事就是让它静止而不是实例:

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

这改变了一些事情:

  • 堆栈语义保持可比性(它加载引用,2个整数和一个bool)
  • callvirt变为call - 这可以避免一些轻微的开销
  • ldarg0 / ldfld对(this.pixelChanged)成为一个ldarg0

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

pixelChanged(x, y, enabled);

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

答案 1 :(得分:4)

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

答案 2 :(得分:3)

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

答案 3 :(得分:1)

正如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);
      }
  }
}