(誤)最適化
レジスタへの読み書きはかなり特殊です。私は、これは副作用そのものの体現だとさえ言っても
よいと思っています。前の例では、同じレジスタに 4 つの異なる値を書き込みました。その
アドレスがレジスタだと知らなければ、最後の値
0x00000000 をレジスタに書き込むだけにロジックを単純化してしまっていたかもしれません。
実際には、コンパイラのバックエンド / オプティマイザである LLVM は、私たちがレジスタを扱っていることを知らず、 書き込みを統合してしまうため、プログラムの動作が変わってしまいます。それを手早く確認してみましょう。
まず、cargo objdump を使って、最適化ビルドと 非最適化ビルドの両方のビルド成果物のアセンブリを取得します。
# 非最適化
cargo objdump -- --disassemble --no-show-raw-insn --source > debug.dump
# 最適化あり
cargo objdump --release -- --disassemble --no-show-raw-insn --source > release.dump
中身を見てみましょう。具体的には、OUT
レジスタを操作しているアセンブリを探してみます。
まず、非最適化ビルドのアセンブリである debug.dump の内容を見てみましょう。
かなりの部分を飛ばし、; <-- の後ろに私のコメントを追加しています。これは、その命令に対応するソース
コードの行番号を示しています。
$ cat debug.dump
[...]
00000158 <main>:
158: push {r7, lr}
15a: mov r7, sp
15c: bl 0x160 <registers::__cortex_m_rt_main::h0b7888ca966441cf> @ imm = #0x0
00000160 <registers::__cortex_m_rt_main::h0b7888ca966441cf>:
160: push {r7, lr}
162: mov r7, sp
164: sub sp, #0x8
166: bl 0x198 <registers::init::hb6346637538e8ec5> @ imm = #0x2e
16a: movw r1, #0x504 ; <-- `OUT` レジスタのアドレスの下位半分をレジスタ `r1` に読み込む
16e: movt r1, #0x5000 ; <-- `OUT` レジスタのアドレスの上位半分をレジスタ `r1` に読み込む
172: str r1, [sp, #0x4]
174: ldr r0, [r1] ; <-- (16) `r1` にあるアドレスの値を `r0` に読み込む。
176: orr r0, r0, #0x200000 ; <-- (16) `r0` の値と `0x200000` のビット単位 OR を取り、その結果を `r0` に格納する
17a: str r0, [r1] ; <-- (16) `r0` の内容を、`r1` にあるアドレスが指すメモリに格納する
17c: ldr r0, [r1] ; <-- (19) `r1` にあるアドレスの値を `r0` に読み込む。
17e: orr r0, r0, #0x80000 ; <-- (19) `r0` の値と `0x80000` のビット単位 OR を取り、その結果を `r0` に格納する
182: str r0, [r1] ; <-- (19) `r0` の内容を、`r1` にあるアドレスが指すメモリに格納する
184: ldr r0, [r1] ; <-- (22) `r1` にあるアドレスの値を `r0` に読み込む。
186: bic r0, r0, #0x200000 ; <-- (22) `r0` の値と `0x200000` をビット反転した値のビット単位 AND を取り、その結果を `r0` に格納する
18a: str r0, [r1] ; <-- (22) `r0` の内容を、`r1` にあるアドレスが指すメモリに格納する
18c: ldr r0, [r1] ; <-- (25) `r1` にあるアドレスの値を `r0` に読み込む。
18e: bic r0, r0, #0x80000 ; <-- (25) `r0` の値と `0x80000` をビット反転した値のビット単位 AND を取り、その結果を `r0` に格納する
192: str r0, [r1] ; <-- (25) `r0` の内容を、`r1` にあるアドレスが指すメモリに格納する
194: b 0x196 <registers::__cortex_m_rt_main::h0b7888ca966441cf+0x36> @ imm = #-0x2
196: b 0x196 <registers::__cortex_m_rt_main::h0b7888ca966441cf+0x36> @ imm = #-0x4
[...]
ご覧のとおり、非最適化アセンブリには 4 回のロード、4 回のストア、そして 4 つのビット操作 命令があります。 これらは、私たちが書いたコードにきれいに対応しています。では、 最適化されたアセンブリを見てみましょう。
$ cat release.dump
[...]
00000158 <main>:
158: push {r7, lr}
15a: mov r7, sp
15c: bl 0x160 <registers::__cortex_m_rt_main::h1f38525e07b97485> @ imm = #0x0
00000160 <registers::__cortex_m_rt_main::h1f38525e07b97485>:
160: push {r7, lr}
162: mov r7, sp
164: bl 0x17a <registers::init::h4390f1d4f8a071f7> @ imm = #0x12
168: movw r0, #0x504 ; <-- `OUT` レジスタのアドレスの下位半分をレジスタ `r0` に読み込む
16c: movt r0, #0x5000 ; <-- `OUT` レジスタのアドレスの上位半分をレジスタ `r0` に読み込む
170: ldr r1, [r0] ; <-- (?) `r0` にあるアドレスの値を `r1` に読み込む。
172: bic r1, r1, #0x280000 ; <-- (?) `r1` の値と `0x280000` をビット反転した値のビット単位 AND を取り、その結果を `r1` に格納する
176: str r1, [r0] ; <-- (?) `r0` の内容を、`r0` にあるアドレスが指すメモリに格納する
178: b 0x178 <registers::__cortex_m_rt_main::h1f38525e07b97485+0x18> @ imm = #-0x4
[...]
えっ? ロード - ビット操作 - ストアが 1 回あるだけ? 今回は LED の状態が変わりませんでした!
str 命令は、値をレジスタに書き込む命令です。debug(非最適化)
プログラムにはそれが 4 回あり、レジスタへの各書き込みに 1 回ずつ対応していましたが、release(最適化)プログラム
には 1 回しかありません。
LLVM が私たちのプログラムを誤って最適化しないようにするにはどうすればよいでしょうか? 通常の
読み書きではなく volatile 操作を使います(examples/volatile.rs):
#![no_main]
#![no_std]
use core::ptr;
#[allow(unused_imports)]
use registers::entry;
#[entry]
fn main() -> ! {
registers::init();
unsafe {
// A magic address!
const PORT_P0_OUT: u32 = 0x50000504;
// Turn on the top row
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out | 1 << 21);
// Turn on the bottom row
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out | 1 << 19);
// Turn off the top row
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out & !(1 << 21));
// Turn off the bottom row
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out & !(1 << 19));
}
loop {
core::hint::spin_loop();
}
}
では、最適化を有効にして、もう一度 cargo objdump を実行しましょう。
cargo objdump -q --release --example volatile -- --disassemble --no-show-raw-insn > release.volatile.dump
では、中身を見てみましょう。
$ cat release.volatile.dump
[...]
00000158 <main>:
158: push {r7, lr}
15a: mov r7, sp
15c: bl 0x160 <registers::__cortex_m_rt_main::h1f38525e07b97485> @ imm = #0x0
00000160 <registers::__cortex_m_rt_main::h1f38525e07b97485>:
160: push {r7, lr}
162: mov r7, sp
164: bl 0x192 <registers::init::h4390f1d4f8a071f7> @ imm = #0x2a
168: movw r0, #0x504
16c: movt r0, #0x5000
170: ldr r1, [r0]
172: orr r1, r1, #0x200000
176: str r1, [r0]
178: ldr r1, [r0]
17a: orr r1, r1, #0x80000
17e: str r1, [r0]
180: ldr r1, [r0]
182: bic r1, r1, #0x200000
186: str r1, [r0]
188: ldr r1, [r0]
18a: bic r1, r1, #0x80000
18e: str r1, [r0]
190: b 0x190 <registers::__cortex_m_rt_main::h1f38525e07b97485+0x30> @ imm = #-0x4
[...]
おお、見てください! これで 4 回のロード - 操作 - ストアのサイクルが戻ってきました。 GDB を使ってもう一度コードをステップ実行し、volatile 操作が実際に動作する様子を確認してください!