6

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

以下代码的运行速度实际上快了 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 代码差异进行一些分析。(所以这个问题实际上来自哪里)

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

4

4 回答 4

5

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

于 2012-10-30T09:17:18.047 回答
5

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

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

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

于 2012-10-30T09:18:18.020 回答
3

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

于 2012-10-30T09:15:59.203 回答
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);
      }
  }
}
于 2012-10-30T10:06:31.810 回答