Y86-64システムアーキテクチャ実習:命令セット拡張とパフォーマンス最適化手法

ビルド環境の初期化

Y86-64ツールチェーンをコンパイルする際、Lex/Yacc生成ツールが必要です。以下のパッケージインストール後、標準的なMakeflow手順でシミュレータを構築します。

sudo apt-get install bison flex
make

逐次パイプラインにおける制御フロー実装(第1章)

ベースアーキテクチャ上で基本データ構造操作を実装する場合、レジスタ割り当て規則とセーブ/レストの原則が正確に適用される必要があります。

リスト要素合計演算(非再帰)

リンクリストの走査と加算処理を行います。パラメータとして渡されたポインタは%rdiに格納され、累積値用レジスタに%r14を採用します。条件判断には符号なしゼロチェックの特性を利用しandq命令でフラグ更新と同時に判定を行います。

/* iterate_sum - リンクedListの全要素を加算 */
iterate_sum:
    xorq %r14, %r14          # 累積変数初期化
    jmp cond_check           # 先頭ループエントリへ

iter_loop:
    mrmovq (%rdi), %r15      # 現在ノードの値を取得
    addq %r15, %r14          # 累積値に加算
    mrmovq 8(%rdi), %rdi     # 次のノードアドレスへ移動

cond_check:
    andq %rdi, %rdi          # ポインタがNULLか判定
    jne iter_loop            # NULL以外ならループ継続
    ret                      # 結果を%rax(返却値)で終了

リスト合計演算(再帰版)

再帰呼び出しでは、上位フレームが利用していたワークエリアレジスタを上書きすると状態破損につながります。したがって、呼び出前後で保持が必要な一時変数は明示的にスタックに保存する必要があります。

/* recurse_sum - 再帰構造による合計計算 */
recurse_sum:
    pushq %r11               # ワーク変数を退避
    andq %rdi, %rdi          # 基底条件判定
    je base_case             # nullの場合0を返す

    mrmovq (%rdi), %r11      # 現在ノード値を退避バッファに格納
    mrmovq 8(%rdi), %rdi     # 次のノードアドレスへ移動
    call recurse_sum         # 再帰呼び出し

    addq %r11, %rax          # 戻り値と現在の値を加算
    popq %r11                # スタック解放
    ret

base_case:
    xorq %rax, %rax          # 空リストなら0
    popq %r11                # 対応するpushと整合性を取る
    ret

レジスタ保存の指針: 関数呼び出しによって破壊される可能性があり、かつ呼出前後で同一の値が必要である場合のみ、セーブ/レストを実行します。

メモリブロック複製処理

ソース配列から宛先配列への転送を行い、排他的論理和(XOR) checksumを算出します。カウンタ管理とインデックス増分に特化した定数レジスタを用いて構造化しています。

/* block_copy - メモリブロック複製とXORチェックサム計算 */
block_copy:
    xorq %r12, %r12          # チェックサム初期化
    xorq %r13, %r13          # カウンタ初期化
    irmovq $1, %r8           # カウンタ増分定数
    irmovq $8, %r9           # アドレス増分定数

cp_loop:
    andq %rdx, %rdx          # 残サイズ判定
    jle cp_done              # ゼロ以下なら脱出
    mrmovq (%rsi), %r14      # ソース読み出し
    rmmovq %r14, (%rdi)      # デスティネーション書込み
    xorq %r14, %r12          # チェックサム更新
    addq %r9, %rdi           # 宛先ポインタ進捗
    addq %r9, %rsi           # ソースポインタ進捗
    subq %r8, %rdx           # カウンタ減算
    jmp cp_loop

cp_done:
    movq %r12, %rax          # 最終結果を返却値へ移換
    ret

HCLを用いた命令拡張設計(第2章)

Y86-64シーケンシャル実行モデルに即値との加算命令iaddqを追加します。この作業はハードウェア記述言語(HCL)を用いて、各パイプラインステージの制御信号を論理式で定義することを目的としています。

順序実行ロジックのマッピング

  • Fetch: 演算オペランドとしてvalC(即値)とvalP(次に実行される命令アドレス)を準備
  • Decode: ソースレジスタrBの内容をvalBに取り込み
  • Execute: ALU内部でvalE = valC + valBを計算し、条件コードフラグも同時に更新
  • Memory: 不要のためパススルー
  • Write-back: 演算結果をrBレジスタへ反映

制御回路の変更箇所(HCL抜粋)

新命令の追加に伴い、有効コマンド集合やALU入力選択ロジックを更新します。

bool instr_valid = icode in { INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ,
                              IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ, IIADDQ };

word srcB = [
    icode in { IOPQ, IRMMOVQ, IMRMOVQ, IIADDQ } : rB;
    icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
    1 : RNONE;
];

word aluA = [
    icode in { IRRMOVQ, IOPQ } : valA;
    icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ } : valC;
    icode in { ICALL, IPUSHQ } : -8;
    icode in { IRET, IPOPQ } : 8;
    1 : valB;
];

bool set_cc = icode in { IOPQ, IIADDQ };

これらの変更を適用後、シミュレータ再ビルドとテストスイート実行により動作検証を行います。

インストラクションレベルの最適化戦略(第3章)

転送関数ncopy.ysのパフォーマンス向上を目指し、アセンブリ実装とパイプライン定義(pipe-full.hcl)の両面からアプローチします。

ステップ1:即値演算命令の採用

従来のaddqはレジスタ間演算しかサポートしていません。iaddqを利用することで即値との演算が1クロックで完了し、CPEが約15.18から12.70へと改善します。

ステップ2:フラグ更新命令の統合除去

負値判定と減算を一体化できます。iaddq $-1, %rdxの時点で条件コードが更新されるため、独立したandqまたはcmpqによる条件分岐準備を削除可能です。

ステップ3:ループ展開(アンローリング)

1回のイテレーションで複数要素を処理するようにスケジューラを書き換え、分岐予測のペナルティとループオーバーヘッドを削減します。以下の例では6分割展開を採用しています。

# 主要なループ本体部分(6展開版)
ncopy:
    xorq %rax, %rax          # 正の数カウント初期化
    iaddq $-6, %rdx          # チャンク境界テスト
    jl handle_remainder

main_loop:
# --- Block 1 ---
    mrmovq (%rdi), %r15
    rmmovq %r15, (%rsi)
    andq %r15, %r15
    jle blk2_entry
    iaddq $1, %rax
blk2_entry:
# --- Block 2 ~ 6 は同構造でオフセット(+8,+16,...,+40) ---
# ... 省略 ...
blk6_entry:
    rmmovq %r16, 40(%rsi)
    andq %r16, %r16
    jle update_ptrs
    iaddq $1, %rax
update_ptrs:
    iaddq $48, %rdi
    iaddq $48, %rsi
    iaddq $-6, %rdx
    jge main_loop
handle_remainder:
    iaddq $6, %rdx
    je exit_function
# --- Remainder handling similar to main loop ---
exit_function:
    ret

展開次数を6まで上げるとCPEは約9.17まで低下しました。

ステップ4:メモリ参照の重なり(Prefetch Overlay)

パイプラインハザードを回避するため、データの読取りを1サイクル先に提前させます。次のブロックのmrmovqを実行側がrmmovqする間に並列起動させることで、待機クロックを削減します。

blk1_exec:
    mrmovq (%rdi), %r15      # 現在データ取得
    mrmovq 8(%rdi), %r16     # 先行読み込み(Next Fetch)
    rmmovq %r15, (%rsi)      # 現在データ出力
    andq %r15, %r15
    jle blk2_exec
    iaddq $1, %rax
blk2_exec:
    rmmovq %r16, 8(%rsi)     # 先行読み込み済みの値を使用
    andq %r16, %r16
    jle blk3_exec
    iaddq $1, %rax
# ... 以後同様のパターンを反復

この手法によりCPEは8.21まで短縮されました。ただし、過度な先行読み込みはリジスタ依存関係の複雑化を招き、逆効果になるため適切なバランスが求められます。

ステップ5:無効初期化の排除

一部の実装環境では%raxの初回クリア処理が不要となるケースがあります。検証の結果、xorq %rax,%raxを削除しても機能し、CPEが8.14にさらに改善しました。これはプロローグ領域のオーバヘッド削減に寄与しています。

タグ: y86-64 computer-architecture hardware-description-language instruction-level-optimization assembly-language

9月20日 13:59 投稿