3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

地上最速の「大石泉すき」判定コードを書いてみた

Last updated at Posted at 2018-12-09

概要

※今回書いたプログラムはWandboxに置いてます

判定条件

  • そのまま「大石泉すき」という文字列から判定しようとすると文字コードの闇に叩き込まれてしまう
  • そこで、「大」から「き」までに「0」から「4」の数字を入れて、整数型の配列として入力するとする

判定のベースとなるコード

#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;

const size_t LOOP_COUNT = 10000;

int judge_hand(const int digit[]) {
    // ロイヤルストレートフラッシュ
    if (digit[0] == 0 && digit[1] == 1 && digit[2] == 2 && digit[3] == 3 && digit[4] == 4)
        return 0;
    // ファイブカード
    if (digit[0] == digit[1] && digit[1] == digit[2] && digit[2] == digit[3] && digit[3] == digit[4])
        return 1;
    // フォーカード
    int count[] = {0, 0, 0, 0, 0};
    for (int i = 0; i < 5; ++i){
        ++count[digit[i]];
    }
    for (int i = 0; i < 5; ++i){
        if (count[i] == 4)
            return 2;
    }
    // ストレートフラッシュ
    bool flg = true;
    for (int i = 0; i < 5; ++i){
        if (count[i] == 0) {
            flg = false;
            break;
        }
    }
    if (flg) {
        return 3;
    }
    // フルハウス
    bool flg_2 = false;
    bool flg_3 = false;
    for (int i = 0; i < 5; ++i){
        if (count[i] == 2) {
            flg_2 = true;
            continue;
        }
        if (count[i] == 3) {
            flg_3 = true;
            continue;
        }
    }
    if (flg_2 && flg_3) {
        return 4;
    }
    // すき
    if (digit[0] == 3 && digit[1] == 4)
        return 5;
    if (digit[1] == 3 && digit[2] == 4)
        return 5;
    if (digit[2] == 3 && digit[3] == 4)
        return 5;
    if (digit[3] == 3 && digit[4] == 4)
        return 5;
    // スリーカード
    if (flg_3) {
        return 6;
    }
    // ツーペア
    if (flg_2) {
        int digit_count = 0;
        for (int i = 0; i < 5; ++i){
            if (count[i] != 0) {
                ++digit_count;
            }
        }
        if (digit_count == 3) {
            return 7;
        }
    }
    return 99;
}

int main()
{
    cout << "ループ回数:" << LOOP_COUNT << "回" << endl;

    auto start = system_clock::now();
    volatile int result = 99;
    int digit[] = {0, 0, 0, 0, 0};
    for (size_t n = 0; n < LOOP_COUNT; ++n) {
        for (size_t i1 = 0; i1 < 5; ++i1){
            digit[0] = i1;
        for (size_t i2 = 0; i2 < 5; ++i2){
            digit[1] = i2;
        for (size_t i3 = 0; i3 < 5; ++i3){
            digit[2] = i3;
        for (size_t i4 = 0; i4 < 5; ++i4){
            digit[3] = i4;
        for (size_t i5 = 0; i5 < 5; ++i5){
            digit[4] = i5;
            result = 99;
        }}}}}
    }
    auto dur = system_clock::now() - start;
    auto nanosec = std::chrono::duration_cast<std::chrono::nanoseconds>(dur).count();
    cout << "ループ自体の時間:" << nanosec << "ナノ秒(" << (1.0 * nanosec / LOOP_COUNT / 5 / 5 / 5 / 5 / 5) << "ナノ秒/回)" << endl;

    start = system_clock::now();
    for (size_t n = 0; n < LOOP_COUNT; ++n) {
        for (size_t i1 = 0; i1 < 5; ++i1){
            digit[0] = i1;
        for (size_t i2 = 0; i2 < 5; ++i2){
            digit[1] = i2;
        for (size_t i3 = 0; i3 < 5; ++i3){
            digit[2] = i3;
        for (size_t i4 = 0; i4 < 5; ++i4){
            digit[3] = i4;
        for (size_t i5 = 0; i5 < 5; ++i5){
            digit[4] = i5;
            result = judge_hand(digit);
        }}}}}
    }
    dur = system_clock::now() - start;
    auto nanosec2 = std::chrono::duration_cast<std::chrono::nanoseconds>(dur).count() - nanosec;
    cout << "計算時間:" << nanosec2 << "ナノ秒(" << (1.0 * nanosec2 / LOOP_COUNT / 5 / 5 / 5 / 5 / 5) << "ナノ秒/回)" << endl;
}

このコードを使い、

  • Wandboxのclang HEAD 8.0.0にOptimizationを付け、C++1a基準でコンパイルする
  • 5回測った平均を見る
    ことを行ったところ、1回の判定につき28.0246ナノ秒掛かりました。言い換えると毎秒3568万回判定できることになります。ここからどんどん加速させていきます。

アルゴリズムの改良

元の判定コードは長ったらしいので、より簡潔にできないかを考えてみました。
これの平均タイムは17.6106ナノ秒/回となります。計算速度が1.6倍になったことになりますね。

int judge_hand_fast(volatile int digit[]) {
    // 各数字の出現回数
    int count[] = {0, 0, 0, 0, 0};
    for (int i = 0; i < 5; ++i)
        ++count[digit[i]];
    // 出現回数の出現回数
    int count2[] = {0, 0, 0, 0, 0, 0};
    for (int i = 0; i < 5; ++i)
        ++count2[count[i]];
    // 判定
    if (count2[1] == 5) {
        if (digit[0] == 0 && digit[1] == 1 && digit[2] == 2 && digit[3] == 3 && digit[4] == 4)
            // ロイヤルストレートフラッシュ
            return 0;
        else
            // ストレートフラッシュ
            return 3;
    }
    // ファイブカード
    if (count2[5] == 1)
        return 1;
    // フォーカード
    if (count2[4] == 1)
        return 2;
    // フルハウス
    if (count2[3] == 1 && count2[2] == 1)
        return 4;
    // すき
    if (digit[0] == 3 && digit[1] == 4)
        return 5;
    if (digit[1] == 3 && digit[2] == 4)
        return 5;
    if (digit[2] == 3 && digit[3] == 4)
        return 5;
    if (digit[3] == 3 && digit[4] == 4)
        return 5;
    // スリーカード
    if (count2[3] == 1)
        return 6;
    if (count2[2] == 2)
        return 7;
    return 99;
}

また、出現頻度が高い役の判定を先に持ってきた方が高速に処理できますので、少し工夫を施しました。
ただ、これだと平均17.5577ナノ秒/回と、思ったような改善ができませんでした。

int judge_hand_fast2(volatile int digit[]) {
    // 各数字の出現回数
    int count[] = {0, 0, 0, 0, 0};
    for (int i = 0; i < 5; ++i)
        ++count[digit[i]];
    // 出現回数の出現回数
    int count2[] = {0, 0, 0, 0, 0, 0};
    for (int i = 0; i < 5; ++i)
        ++count2[count[i]];
    // 判定
    // ツーペア・すき
    if (count2[2] == 2){
        // すき
        if (digit[0] == 3 && digit[1] == 4)
            return 5;
        if (digit[1] == 3 && digit[2] == 4)
            return 5;
        if (digit[2] == 3 && digit[3] == 4)
            return 5;
        if (digit[3] == 3 && digit[4] == 4)
            return 5;
        // ツーペア
        return 7;
    }
    // スリーカード・すき・フルハウス
    if (count2[3] == 1){
        // すき
        bool sukiFlg = false;
        if (digit[0] == 3 && digit[1] == 4)
            sukiFlg = true;
        else if (digit[1] == 3 && digit[2] == 4)
            sukiFlg = true;
        else if (digit[2] == 3 && digit[3] == 4)
            sukiFlg = true;
        else if (digit[3] == 3 && digit[4] == 4)
            sukiFlg = true;
        // フルハウス
        if (count2[2] == 1)
            return 4;
        if (sukiFlg)
            return 5;
        // スリーカード
        return 6;
    }
    // すき
    bool sukiFlg = false;
    if (digit[0] == 3 && digit[1] == 4)
        sukiFlg = true;
    else if (digit[1] == 3 && digit[2] == 4)
        sukiFlg = true;
    else if (digit[2] == 3 && digit[3] == 4)
        sukiFlg = true;
    else if (digit[3] == 3 && digit[4] == 4)
        sukiFlg = true;
    // ストレートフラッシュ・ロイヤルストレートフラッシュ
    if (count2[1] == 5) {
        if (digit[0] == 0 && digit[1] == 1 && digit[2] == 2 && digit[3] == 3 && digit[4] == 4)
            // ロイヤルストレートフラッシュ
            return 0;
        else
            // ストレートフラッシュ
            return 3;
    }
    // フォーカード
    if (count2[4] == 1)
        return 2;
    // ファイブカード
    if (count2[5] == 1)
        return 1;
    if (sukiFlg)
        return 5;
    return 99;
}

開き直って配列に格納する方法もあります。平均0.32019ナノ秒/回と爆速ですが、ベンチマーク的に考えるとインチキそのものなのが難点。

int cache[3125] = {};

// 判定用関数
int judge_hand_fast3(const int digit[]) {
    return cache[digit[0]*5*5*5*5+digit[1]*5*5*5+digit[2]*5*5+digit[3]*5+digit[4]];
}

// 使用前の初期化
for (size_t i1 = 0; i1 < 5; ++i1){
    digit[0] = i1;
for (size_t i2 = 0; i2 < 5; ++i2){
    digit[1] = i2;
for (size_t i3 = 0; i3 < 5; ++i3){
    digit[2] = i3;
for (size_t i4 = 0; i4 < 5; ++i4){
    digit[3] = i4;
for (size_t i5 = 0; i5 < 5; ++i5){
    digit[4] = i5;
    cache[i1*5*5*5*5+i2*5*5*5+i3*5*5+i4*5+i5] = judge_hand(digit);
}}}}}

SIMDに手を出す

と言ってもIntrinsicを使うのではなく、ただのビット演算になりました。
だって1文字が5通り、つまり3ビット分しかないので5文字合っても15ビットしか使わないんだもの……。
ただ、試したところかえって遅くなってしまいましたorz

inline uint32_t calc_bitboard(const int index){
    return 1 << (index * 3);
}

int judge_hand_fast4(const int digit[]) {
    // 各数字の出現回数をビットボードを利用してカウント
    uint32_t count_board = 0;
    count_board += calc_bitboard(digit[0]);
    count_board += calc_bitboard(digit[1]);
    count_board += calc_bitboard(digit[2]);
    count_board += calc_bitboard(digit[3]);
    count_board += calc_bitboard(digit[4]);
    const int count_0 = count_board & 0x7;
    const int count_1 = (count_board >>  3) & 0x7;
    const int count_2 = (count_board >>  6) & 0x7;
    const int count_3 = (count_board >>  9) & 0x7;
    const int count_4 = (count_board >> 12) & 0x7;
    
    // 各数字の出現回数の出現回数をビットボードを利用してカウント
    count_board = 0;
    count_board += calc_bitboard(count_0);
    count_board += calc_bitboard(count_1);
    count_board += calc_bitboard(count_2);
    count_board += calc_bitboard(count_3);
    count_board += calc_bitboard(count_4);
    const int count2_0 = count_board & 0x7;
    const int count2_1 = (count_board >>  3) & 0x7;
    const int count2_2 = (count_board >>  6) & 0x7;
    const int count2_3 = (count_board >>  9) & 0x7;
    const int count2_4 = (count_board >> 12) & 0x7;
    const int count2_5 = (count_board >> 15) & 0x7;
        
    // 判定
    // ツーペア・すき
    if (count2_2 == 2){
        // すき
        if (digit[0] == 3 && digit[1] == 4)
            return 5;
        if (digit[1] == 3 && digit[2] == 4)
            return 5;
        if (digit[2] == 3 && digit[3] == 4)
            return 5;
        if (digit[3] == 3 && digit[4] == 4)
            return 5;
        // ツーペア
        return 7;
    }
    // スリーカード・すき・フルハウス
    if (count2_3 == 1){
        // フルハウス
        if (count2_2 == 1)
            return 4;
        // すき
        bool sukiFlg = false;
        if (digit[0] == 3 && digit[1] == 4)
            return 5;
        else if (digit[1] == 3 && digit[2] == 4)
            return 5;
        else if (digit[2] == 3 && digit[3] == 4)
            return 5;
        else if (digit[3] == 3 && digit[4] == 4)
            return 5;
        // スリーカード
        return 6;
    }
    // ストレートフラッシュ・ロイヤルストレートフラッシュ
    if (count2_1 == 5) {
        if (digit[0] == 0 && digit[1] == 1 && digit[2] == 2 && digit[3] == 3 && digit[4] == 4)
            // ロイヤルストレートフラッシュ
            return 0;
        else
            // ストレートフラッシュ
            return 3;
    }
    // フォーカード
    if (count2_4 == 1)
        return 2;
    // ファイブカード
    if (count2_5 == 1)
        return 1;
    // すき
    if (digit[0] == 3 && digit[1] == 4)
        return 5;
    else if (digit[1] == 3 && digit[2] == 4)
        return 5;
    else if (digit[2] == 3 && digit[3] == 4)
        return 5;
    else if (digit[3] == 3 && digit[4] == 4)
        return 5;
    return 99;
}

計測結果の詳細

コード 1回目 2回目 3回目 4回目 5回目 平均
デフォ 28.2191 27.0735 30.0721 26.8518 27.9063 28.0246
fast 16.9103 17.5591 18.2040 17.1986 18.1810 17.6106
fast2 17.5094 17.6602 17.6207 17.1255 17.8726 17.5577
fast3 0.240192 0.475264 0.345120 0.279904 0.260448 0.320186
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?