なが月・25日目

25日目

午前

並列プログラミング入門、今日はreductionを実際に書いて確かめた。

#include <bits/stdc++.h>
#include <omp.h>

int main() {
    std::vector<int> a = {1, 2, 3, 4, 5, 6, 7, 8};
    std::vector<int> b = {1, 2, 3, 4, 5, 6, 7, 8};
    int dotProduct = 0; // 1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 = 204
    
    // sequential
    std::cout << "===sequential===" << "\n";
    for (int i = 0; i < 8; ++i) {
        dotProduct += a[i] * b[i];
    }
    std::cout << dotProduct << "\n";
    dotProduct = 0;

    // parallel w/o reduction
    std::cout << "===parallel with no reduction===" << "\n";
#pragma omp parallel for
    for (int i = 0; i < 8; ++i) {
        dotProduct += a[i] * b[i];
        printf("adding: %d, dotProduct is now: %d\n", a[i] * b[i], dotProduct);
    }
    std::cout << dotProduct << "\n";
    dotProduct = 0;

    // parallel w/ reduction
    std::cout << "===parallel with reduction===" << "\n";
#pragma omp parallel for reduction (+: dotProduct)
    for (int i = 0; i < 8; ++i) {
        dotProduct += a[i] * b[i];
    }
    std::cout << dotProduct << "\n";

    std::cout << "===END===" << "\n";
    return 0;
}

ここでreductionなしの並列実行for文中はstd::cout << "adding: " << a[i] * b[i] ...とはしていない。あるスレッドで一個出力演算子を処理してバッファに出している間に、他のスレッドも同じようにバッファに出してしまうので、結果がadding: adding: adding: 49adding: adding: 16...などとなってしまったからだ。命令が演算子単位で実行されることを実感できるよい経験だった。

そして実行結果はこのようになった。

===sequential===
204
===parallel with no reduction===
adding: 1, dotProduct is now: 1
adding: 9, dotProduct is now: 9
adding: 64, dotProduct is now: 178
adding: 25, dotProduct is now: 25
adding: 16, dotProduct is now: 45
adding: 4, dotProduct is now: 29
adding: 49, dotProduct is now: 114
adding: 36, dotProduct is now: 65
178
===parallel with reduction===
204
===END===

計算途中の内積が1, 9というのはまだ理解できる。各スレッドで行っているのはdotProduct = dotProduct + a[i] * b[i]であり、最初の二行においてはまだdotProductは0が入っていたのだろう。ところが178以降はめちゃくちゃになっている。178 = 114 + 64で、25 = 0 + 25、45 = 29 + 16、29 = 25 + 4、114 = 65 + 49、65 = 29 + 36である。

すなわちスレッド番号(=a[i])を1~8とすると、スレッドNo.1, 3, 5が取得した時点ではdotProductは0であり、順に上書きして25となった。それからNo.2が25を取得し29を代入、No.4が29を取得し45を代入、No.6がNo.4の代入前に29を取得し65を代入、No.7、No.8と続いたということだ。

最も基本的な加算処理v += 1でさえ、数取器を1回押しているのではなく(インクリメント演算子はそうしてくれるのだろうか?)その時点での値をメモして、それに1を足した数に書き換えているという処理の内実が表れている(当たり前だが)。逐次実行の独立性に慣れ親しんでいた身にはなんとも恐ろしい話である。

そこでreductionとすると、加算処理をそのまま行わずに「dotProductに足したい数」を足し合わせて、最後にdotProductに加算を行う。乗算の場合は掛け合わせて最後に乗算を行うという仕組みだ。

午後

IEventSystemHandlerを継承してメッセージングシステムを構築しようとしていた時、ふとC++のArgument Dependent Lookupに相当するものはあるのかと引数だけが違う同名関数宣言を書いてみると問題なく通り、実装部分で二つ実装してから呼び出しに引数を与えると対応した引数をとる方の関数が呼ばれていることが分かった。これをC#ではオーバーロードと言うようだ。Pythonはどうだったかと調べるとそのような仕様はないらしい。

ExecuteEvents.ExecuteはIEventSystemHandlerを継承したインタフェースを継承している全てのスクリプトインスタンスでメソッドを呼び出すので、スポーンした全車両に対して命令を行うなど、GetComponent<T>で全ての対象インスタンスを保持しておくのがスマートとは言えない場合に使えそうなことが分かった。明日は引き続きレベル目標を設定できるようにする実装を続ける。