19

「RayW ハンド エバリュエーター」アプローチを使用して、カードの組み合わせスコア (7 枚中 5 枚のベスト カード) を取得しようとしています。ただし、この方法にはいくつかのパフォーマンスの問題があります。情報源によると、このアプローチを使用すると、1 秒あたり 3 億以上のハンドを評価できるはずです。私の結果は 1.5 秒で 10 ミルで、何倍も遅くなります。

「RayW ハンド エバリュエーター」の背後にある考え方は次のとおりです。

Two Plus Two エバリュエーターは、約 3200 万のエントリ (正確には 32,487,834) を含む大きなルックアップ テーブルで構成されています。特定の 7 カード ポーカー ハンドをルックアップするには、このテーブルのパスをたどり、カードごとに 1 回のルックアップを実行します。最後のカードに到達すると、得られた値がハンドの公式等価値です。

コードは次のようになります。

namespace eval
{
public struct TPTEvaluator
{
    public static int[] _lut;

    public static unsafe void Init() // to load a table
    {
        _lut = new int[32487834];
        FileInfo lutFileInfo = new FileInfo("HandRanks.dat");
        if (!lutFileInfo.Exists)
        {throw new Exception("Handranks.dat not found");}

        FileStream lutFile = new FileStream("HandRanks.dat", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096);

        byte[] tempBuffer = new byte[32487834 * 4];
        lutFile.Read(tempBuffer, 0, 32487834 * 4);

        fixed (int* pLut = _lut)
        { Marshal.Copy(tempBuffer, 0, (IntPtr)pLut, 32487834 * 4);}
        tempBuffer = null;
    }

    public unsafe static int LookupHand(int[] cards) // to get a hand strength
    {
        fixed (int* pLut = _lut)
        {
            int p = pLut[53 + cards[0]];
            p = pLut[p + cards[1]];
            p = pLut[p + cards[2]];
            p = pLut[p + cards[3]];
            p = pLut[p + cards[4]];
            p = pLut[p + cards[5]];
            return pLut[p + cards[6]];
        }
    }
}

}

そして、それが私がこのアプローチをテストする方法です:

    private void button4_Click(object sender, EventArgs e)
    {
        int[] str = new int[] { 52, 34, 25, 18, 1, 37, 22 };

        int r1 = 0;

        DateTime now = DateTime.Now;
        for (int i = 0; i < 10000000; i++) // 10 mil iterations 1.5 - 2 sec
        { r1 = TPTEvaluator.LookupHand(str);} // here
        TimeSpan s1 = DateTime.Now - now;
        textBox14.Text = "" + s1.TotalMilliseconds;
    }

このメソッドは元々 C++ で実装されていたと思いますが、それでも C# ポートはより高速に動作するはずです。1 秒間に少なくとも 1 億ハンドに近づく方法はありますか?

私がこれまでに試したこと:

  • 静的メソッドと非静的メソッドを使用してみました-違いはありません。
  • 配列の代わりに辞書検索を使用してみました

    public void ArrToDict(int[] arr, Dictionary<int, int> dic)
    {
        for (int i = 0; i < arr.Length; i++)
        {
            dic.Add(i, arr[i]);
        }
    }
    
    public unsafe static int LookupHandDict(int[] cards)
    {
        int p = dict[53 + cards[0]];
        p = dict[p + cards[1]];
        p = dict[p + cards[2]];
        p = dict[p + cards[3]];
        p = dict[p + cards[4]];
        p = dict[p + cards[5]];
        return dict[p + cards[6]];
    }
    

10 ミルのハンドの経過時間は、ほぼ 6 倍遅くなります。

  • ある人によると、「安全でない」コードを削除することで、パフォーマンスが 200 ミル向上しました。同じことをやってみましたが、結果はほぼ同じです。

    public static int LookupHand(int[] cards)
    {
            int p = _lut[53 + cards[0]];
            p = _lut[p + cards[1]];
            p = _lut[p + cards[2]];
            p = _lut[p + cards[3]];
            p = _lut[p + cards[4]];
            p = _lut[p + cards[5]];
            return _lut[p + cards[6]];
    }
    

引用は次のとおりです。

「安全でない」コード部分を削除し、c# バージョンでいくつかの小さな調整を行った後、現在も約 310 ミオです。

このハンド ランキング システムのパフォーマンスを向上させる他の方法はありますか?

4

2 に答える 2

4

まず、ベンチマークは常に注意が必要です。自分のマシンで一方向に実行されるものは、他のマシンでも常に同じように実行されるわけではなく、データを無効にする可能性のある「隠れた」状態がたくさんあります(OSやハードウェアによって行われるキャッシュなど)。

そうは言っても、私はあなたのInit()メソッドだけを調べたところ、頭をかきむしりました。ついていくのが難しいと思いました。「安全でない」を使用するための私の親指のルールは、絶対に必要でない限り、それを使用しないことです。このInit()メソッドは、私が想定しているように、一度呼び出されますよね?私はそれをベンチマークすることにしました:

static void BenchmarkIt(string input, Action myFunc)
{
    myWatch.Restart();
    myFunc();
    myWatch.Stop();

    Console.WriteLine(input, myWatch.ElapsedMilliseconds);
}

BenchmarkIt("Updated Init() Method:  {0}", Init2);
BenchmarkIt("Original Init() Method: {0}", Init1);  

ここで、Init1()は元のコードであり、Init2()は私の書き直されたコードです(公平を期すために、順序を数回入れ替えました)。これが私が(私のマシンで)得たものです...

更新されたInit()メソッド:110

元のInit()メソッド:159

これが私が使用したコードです。安全でないキーワードは必要ありません。

public static void Init2()
{
    if (!File.Exists(fileName)) { throw new Exception("Handranks.dat not found"); }            

    BinaryReader reader = new BinaryReader(File.Open(fileName, FileMode.Open));            

    try
    {
        _lut = new int[maxSize];
        var tempBuffer = reader.ReadBytes(maxSize * 4); 
        Buffer.BlockCopy(tempBuffer, 0, _lut, 0, maxSize * 4);
    }
    finally
    {
        reader.Close();
    }
}

私の意見では、このコードは読みやすく、実行速度も速いようです。

LookupHand()のパフォーマンスについてもっと心配されていると思いますが、大幅な改善はできませんでした。私はいくつかの異なるアプローチを試しましたが、何も役に立ちませんでした。

私はあなたのコードを500ミリ秒で1億回実行することができました。私はかなり頑丈な64ビットラップトップで実行しています-これはあなたが期待していた速度のようです。他の人が言っているように、リリースモードで実行する(最適化を有効にする)と、パフォーマンスに大きな影響を与える可能性があります。

于 2012-05-07T20:26:02.913 に答える
4

一般的な速度が必要な場合は、Brecware の評価ツールを使用することをお勧めします: https://web.archive.org/web/20160502170946/http://brecware.com/Software/software.html。Steve Brecher のエバリュエーターは、ランダムな順序で発生する評価について RayW エバリュエーターよりも高速であり、はるかにコンパクトです。

コメントで述べたように、RayW エバリュエーターは、その速度について参照の局所性に依存します。ルックアップ テーブルとまったく同じ順序で評価をたどらないと、処理が遅くなります。それがあなたの問題である場合、3つのアプローチがあります:

  1. 評価順序をテーブルに近づけてください。
  2. 評価順序に一致するテーブルを作成する
  3. ユースケースに最適化されたエバリュエーターを作成します。
于 2012-05-09T00:38:23.117 に答える