kdnakt blog

hello there.

Rust dojo第80回を開催した

第80回です。前回はこちら。

kdnakt.hatenablog.com

 

[第80回の様子]

2023/06/07に第80回を開催した。

内容としてはRust By Example 日本語版22. 安全でない操作の「22.1. Inline assembly」のLate output operandsに取り組んだ。
参加者は自分を入れて7人。前回途中で終わってしまったので、今回もドライバを担当した。
今回から今年の新卒メンバーも2人参加してくれた。アセンブリ普通にわかるっぽくて強い...💪

[学んだこと]

  • 22.1. Inline assembly: Late output operands
  • Rustのコンパイラオペランド(被演算子)の割り当てに保守的
    • outの変数は常時書き込み可能として扱う
    • つまり、他の変数とレジスタを共有しない(できない)
  • しかし、パフォーマンスを最適化するには、利用するレジスタを最小化する
  • そのためにRustにはlateout指定子がある
    • 全ての入力引数が消費された後でのみ出力の書き込みに利用可能
    • inlateout指定子もある
  • 次の例はinlateoutを利用できないケース
use std::arch::asm;

let mut a: u64 = 4;
let b: u64 = 4;
let c: u64 = 4;
unsafe {
    asm!(
        "add {0}, {1}",
        "add {0}, {2}",
        inout(reg) a,
        in(reg) b,
        in(reg) c,
    );
}
assert_eq!(a, 12);
  • 最適化されていないdebugモードではinlateoutを利用しても動作するが、最適化されたreleaseモードだとinlateoutを利用すると動作しない
  • inlateoutを利用してreleaseモードで動作させると以下のようになる
use std::arch::asm;

let mut a: u64 = 4;
let b: u64 = 4;
let c: u64 = 4;
unsafe {
    asm!(
        "add {0}, {1}",
        "add {0}, {2}",
        inlateout(reg) a,
        in(reg) b,
        in(reg) c,
    );
}
assert_eq!(a, 12);

// 以下のようにパニックする
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `16`,
 right: `12`', src/main.rs:17:1
  • 最適化すると、コンパイラがbとcが同じ値なので同じレジスタを利用する
  • inoutを利用している場合、コンパイラはaに別のレジスタを割り当てる
  • しかしinlateoutを利用するとaとcが同じレジスタに割り当てられる
  • そのため、4 + 4 (a + b)の結果がaに(つまりcに)割り当てられ、最後の計算が8 + 8となってしまう
  • つぎのように、全ての入力レジスタが読み込まれた後に出力に書き込まれる場合は、inlateoutを利用しても問題がない
use std::arch::asm;

let mut a: u64 = 4;
let b: u64 = 4;
unsafe {
    asm!("add {0}, {1}", inlateout(reg) a, in(reg) b);
}
assert_eq!(a, 8);
  • ところで、最初の例にprintln!マクロを利用して変数の中身を出力している内に、println!マクロの有無でコードの実行結果が変わる実例に出会ってしまった
use std::arch::asm;

fn main() {
    let mut a: u64 = 4;
    let mut b: u64 = 4;
    let mut c: u64 = 4;

    // 消すと--releaseビルドでバグる
    println!("a={}", a);

    unsafe {
        asm!(
            "add {0}, {1}",
            "add {0}, {2}",
            inlateout(reg) a,
            in(reg) b,
            in(reg) c,
        );
    }
    println!("a={}", a);
    // 最初のprintln!マクロありのreleaseビルド:a=12
    // 最初のprintln!マクロなしのreleaseビルド:a=16
}
  • 上記のコードはこちらのplaygroundで実行できる
  • おそらく、最初のprintln!で変数aを利用するかどうかで、cと異なるレジスタが割り当てられるかどうかが変わるのだろう
  • なぜそうなるのかはよく分からないが...

[まとめ]

モブプログラミングスタイルでRust dojoを開催した。
レジスタむずかしい...。

今週のプルリクエストはこちら。

github.com