この辺の実装がどのようになっているのかを勉強したい。ChatGPTにお願いしながら、読み進めていくことにする。
5. テーブルと内積計算(std::inner_product)
result.yout = std::inner_product( std::begin(tables), std::end(tables), std::begin(result.indices), 0, std::plus<>{}, [](const auto &table, const auto &index) { return table.at(index).value(); } );
tables:複数個の小さな重みテーブルを指す配列である。各テーブルは、インデックスを与えると「重み(weight)」が返ってくる構造(.at(index).value()で値を取り出せる)となっている。- 典型的には、各テーブルは2N個の「スコア」(たとえば8ビットor16ビットの整数)を持っており、
table.at(i).value()でその重みが得られる。 - テーブルの数は、グローバル履歴を分割したワード数と対応しており、たとえば8個の履歴ワードならテーブルも8個、という配置が多い。
std::inner_productの引数説明:
std::begin(tables), std::end(tables):テーブル列を走査するためのイテレータ範囲std::begin(result.indices):各テーブルに対して対応する「インデックス」を使用するためのイテレータ0:累積和の初期値(ここでは整数0で初期化)std::plus<>{}:結果の加算方法(重みを足し合わせる)- ラムダ
[](const auto &table, const auto &index) { return table.at(index).value(); }- これは「テーブルとインデックスを受け取って、その位置の重みを返す関数」である。
- つまり、
table.at(index).value()が返す整数(signedかunsignedかは実装次第)を、std::plus'で累積していく。
結果としてresult.youtには、各テーブルから取り出した重みの合計(内積)が入る。これが「パーセプトロンの出力値」に相当する。
補足: std::inner_product() のラムダで指定されている2つの引数は、
- table は std::begin(tables) から std::end(tables) までをたどる際の 「各ループで得られるテーブル(=tables[i])」
- index は同じループ順序で std::begin(result.indices) から得られる 「各テーブルに対応するインデックス(=result.indices[i])」
6. 結果の保存と予測
last_result = result;
return result.yout >= THRESHOLD;
last_result:クラスメンバーで、直前に計算したパーセプトロンの出力やインデックス情報を保持しておく変数である。学習(トレーニング)時に、この直前の結果を使用して重みの更新を行うことが多いため保存している。- 最終的に計算した合計スコア
youtが、あらかじめ定められた閾値THRESHOLD以上であればtrue(分岐はtakenと予測)、未満ならfalse(not takenと予測)を返す。
7. 一連の動作イメージ
- 呼び出し時にpcが渡される → その下位ビットをpc_sliceに取り出す
- グローバル履歴を数ビットずつ区切った配列ghist_words(例:8要素)を持っており、各要素に対して「ヒストリービット ^ pc_slice」を計算してresult.indicesに格納
- たとえばghist_words = {h0, h1, h2, …, h7}だったとすると:
result.indices[0] = h0.value() ^ pc_slice; result.indices[1] = h1.value() ^ pc_slice; … result.indices[7] = h7.value() ^ pc_slice;
- 各インデックスを使用して、対応するテーブルから重みを取り出し、すべて合算 → yout(整数)になる
- yout >= THRESHOLDなら「分岐taken」と予測しtrueを返す。そうでなければfalseを返す。