循环中的方法调用的开销是什么?



我已经在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秒。由于迷宫生成器代码当前由许多单独的方法呼叫组成,这些调用会引起不同的动作,因此开始获得大量弹药。

我还检查了Google和堆栈溢出的其他帖子,但尚未真正找到解决方案。

是否可以自动优化这样的代码?(也许有像Roslyn项目之类的东西??),或者我应该以一种大方法将所有内容放在一起?

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

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

这是一个问题,JIT具有对方法的内联优化(其中实际方法代码实际上是在调用父代码中注入的),但是这仅发生在编译为32字节的方法中或更少。我不知道为什么存在32个字节限制,并且还希望在C/C 中看到一个"内联"关键字,以解决这些问题。

我要尝试的第一件事就是使其静态而不是实例:

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

这改变了一些事情:

  • 堆栈语义保持可比性(它加载了参考,2个INT和BOOL)
  • callvirt变为 call-避免了一些小开销
  • ldarg0/ldfld对(this.pixelChanged)成为单个ldarg0

我要看的接下来是PixelChangedEventArgs;如果避免了很多分配,则可以将其作为结构更便宜。也许只是:

pixelChanged(x, y, enabled);

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

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

正如马克所说的那样,主要开销是进行虚拟呼叫和通过参数。在执行方法期间,Pixelchange的变化值可以吗?如果不能,这可能不起作用(我不确定JIT优化了空的动作委托,您必须自己测试(如果不这样做,我只是在这里无视良好的做法通话,一个带有pixelchanged的通话。召唤,一个没有(直列)的电话,只需打电话给任何最适合的东西...毕竟,有时您必须使代码变得更加优雅,以使其快速)。

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

最新更新