6

私はしばらくの間、128000x128000 ピクセルのような迷路を生成できる C# 迷路ジェネレーターに取り組んできました。すべてのメモリ使用量は既に最適化されているため、現在、生成の高速化を検討しています。

私が見つけた問題(興味のある点からはかなり外れています)は次のとおりです(問題を説明するためのサンプルコードにすぎません):

このコードは、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 のようなものでしょうか???) それとも、すべてを 1 つの大きなメソッドにまとめる必要がありますか?

編集:これら2つのケースでの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 つの int、および bool をロードします)。
  • callvirtなる- これにより、callいくつかのマイナーなオーバーヘッドが回避されます
  • ldarg0/ldfldペア ( this.pixelChanged) が 1 つになるldarg0

私が次に見たいのはPixelChangedEventArgs; 多くの割り当てを回避する場合、それを構造体として渡す方が安価である可能性があります。またはおそらく単に:

pixelChanged(x, y, enabled);

(ラッパー オブジェクトではなく生の引数 - 署名の変更が必要)

于 2012-10-30T09:18:18.020 に答える
3

これはデバッグ モードですか、それともリリース モードですか? メソッド呼び出しはかなりコストがかかりますが、リリース モードでビルド/実行するとインライン化される場合があります。デバッグ モードでは、コンパイラから最適化されません。

于 2012-10-30T09:15:59.203 に答える
1

マークが言ったように、主なオーバーヘッドは、その仮想呼び出しを行い、引数を渡すことです。メソッドの実行中に 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 に答える