結論から言うとモデルを作っているのだけれども、例えばRISC-VのMULH命令などの計算結果の上位ビットを持ってくるようなタイプの命令は、最初に何が正しいのかナイーブな方法で計算しておかないと訳が分からなくなる。
一つのステップは、riscv-isa-sim (Spike) が完全であるとまず信用すること。自分のハードウェアとSpikeが完全に同じ答えを返すように作り込むのが大前提。 それでもってSpikeとハードウェアの答えが不一致となった場合にどうするのか、という問題だが、MULH命令の答えが合わない場合は上位64ビットしか出てこないのでデバッグが地獄になってしまう。
そこで、64ビット×64ビット=128の答えを計算するモデルを作っておき、ハードウェアがどういう動作をすべきなのか、比較できるようにしておく。 ここでそのためのモデルはC++を使って記述している(多分PythonとかRubyを使った方が賢い)、そしてgmpを使って64ビットを超える計算ができるようにしている。
ちなみにこのgmpを使う方法はSpikeが行っている方法そのままだ。本来はもうちょっと簡単にやるべきなのかもしれない。
... mpz_class *rs1_mpz = new mpz_class(rs1_val, 16); mpz_class *rs2_mpz = new mpz_class(rs2_val, 16); static int unroll = 8; for (int j = 64; j >= 0; j-=unroll) { printf (" "); } std::cout << std::setw(64/4) << std::setfill('0') << std::hex << *rs1_mpz << '\n'; for (int j = 64; j >= 0; j-=unroll) { printf (" "); } std::cout << std::setw(64/4) << std::setfill('0') << std::hex << *rs2_mpz << '\n'; for (int j = 64*2; j >= 0; j-=unroll) { printf ("--"); } std::cout << '\n'; mpz_class prod_ans = 0; for (int i = 0; i < 64; i += unroll) { mpz_class mplier = *rs1_mpz >> i & ((1 << unroll)-1); mpz_class part_pro = mplier * *rs2_mpz; std::cout << std::setw((64-i)/4) << std::setfill(' ') << " "; std::cout << std::setw((64+unroll)/4) << std::setfill('0') << std::hex << part_pro.get_str(16) << '\n'; prod_ans += part_pro << i; std::cout << std::setw((128+unroll)/4) << std::setfill('0') << std::hex << prod_ans.get_str(16) << '\n'; }
こんな感じで、8ステージの乗算器ならば、どういう途中結果を出せばいいのかモデル化しておく。
本当はもっと簡潔なアルゴリズムがあるのかもしれないが、ナイーブな方法を取っている。また、この検証も出る自体も符号付きなどに対応する必要があるため、もう少し改良する必要があるだろう。