Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

(誤)最適化

レジスタへの読み書きはかなり特殊です。私は、これは副作用そのものの体現だとさえ言っても よいと思っています。前の例では、同じレジスタに 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 操作が実際に動作する様子を確認してください!