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

C# 循环中方法调用的开销是多少?,c#,performance,compiler-construction,C#,Performance,Compiler Construction,我一直在研究一个C#迷宫生成器,它可以生成128000x128000像素的迷宫。所有的内存使用都已经优化了,所以我目前正在考虑加快生成速度 我发现的一个问题(更偏离兴趣点)如下(只是一些示例代码来说明这个问题): 当pixelChanged为空时,此代码在我的机器上运行约1.4秒: public void Go() { for (int i = 0; i < bitjes.Length; i++) { BitArray curArray = bitjes[i

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

我发现的一个问题(更偏离兴趣点)如下(只是一些示例代码来说明这个问题):

当pixelChanged为空时,此代码在我的机器上运行约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));
    }
}
public void Go()
{
for(int i=0;i
其中,以下代码的运行速度实际上快了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));
            }
        }
    }
}
public void Go()
{
for(int i=0;i
似乎当只调用“empty”方法时,该算法占用了大约20%的cpu。这不是很奇怪吗?我试图在调试和发布模式下编译解决方案,但没有发现任何明显的差异

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

我也检查了谷歌和其他关于堆栈溢出的帖子,但还没有真正找到解决方案

有可能像这样自动优化代码吗?(可能是Roslyn项目?)或者我应该用一个大方法把所有的东西放在一起吗

编辑: 我还想对这两种情况下JIT/CLR代码的差异进行一些分析。(那么这个问题究竟从何而来)

编辑2:
所有代码都是在发布模式下编译的

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

这是一个问题,JIT对方法有一个内联优化(其中整个方法代码实际上被注入调用父代码中),但这只发生在编译为32字节或更少的方法上。我不知道为什么会存在32字节的限制,我也想在C#中看到一个“inline”关键字,就像在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
接下来我要看的是
pixelchangedventargs
;如果它避免了大量的分配,那么将其作为结构传递可能会更便宜;或者也许只是:

pixelChanged(x, y, enabled);

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

正如Marc所说,主要开销是进行虚拟调用并传递参数。在方法执行过程中,像素值是否会发生变化?如果没有,这可能会起作用(我不完全确定JIT是否将空操作委托优化为nop,您必须自己测试它(如果没有,我将忽略这里的良好实践,只进行调用,一个使用pixelChanged.Invoke调用,一个不使用(内联)调用)只要调用最合适的代码就行了……毕竟,有时候你必须让代码变得不那么优雅,这样才能更快)

public void Go()
{
如果(像素已更改!=null)
GoPixelGo((x,y,z)=>{});
其他的
GoPixelGo((i,y,enabled)=>pixelChanged.Invoke(i,y,enabled));
}
公开作废GoPixelGo(行动)
{
for(int i=0;i
我已经查看了像素更改(x、y、启用)这确实加快了速度。您可以解释一下callvirt和call差异的含义吗?以及ldarg0/ldfld对?@Devedse在实例方法中,您可以访问
这一点。pixelChanged
两次,就好像我们在一个参数中传递它一样,如果它只做了一次;一旦引用作为本地或参数在堆栈上(在本例中为arg0),它的速度稍微快一点;此外,如果我们将其设置为静态而非实例,则必须将其设置为参数。我尝试了您的代码,现在它比0.4慢了约0.25秒。我们正在改进:)我在release和debug中都试过。在调用方法的情况下,这两种方法都慢了大约0.4秒。@Devedse:你是如何内联这些方法的?@Luis Filipe:正如你在主主题中看到的那样,我手动复制了代码。我从未听说过内联关键字,但它似乎正是我所需要的:),我希望微软有一天也能将其添加到C#中。我曾尝试将[MethodImpl(MethodImplOptions.AggressiveInline)]放在我的GoDrawPixel(…)方法前面,但没有成功。它仍然比直接实现慢0.4秒。我正在使用它,但它没有任何区别:/我希望有人能帮我解决这个问题,因为这似乎是最有希望的答案,如果
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);
      }
  }
}