Ошибочная оптимизация
Чтения и записи в регистры — особенная вещь. Осмелюсь даже сказать, что они воплощают побочные эффекты. В предыдущем примере мы четыре раза записали разные значения в один и тот же регистр. Если бы вы не знали, что этот адрес — регистр, вы могли бы упростить логику и просто записать в регистр итоговое значение 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`, результат сохраняем в `r0`
17a: str r0, [r1] ; <-- (16) Записываем содержимое `r0` в память по адресу из `r1`
17c: ldr r0, [r1] ; <-- (19) Загружаем значение по адресу из `r1` в `r0`.
17e: orr r0, r0, #0x80000 ; <-- (19) Побитовое ИЛИ значения `r0` с `0x80000`, результат сохраняем в `r0`
182: str r0, [r1] ; <-- (19) Записываем содержимое `r0` в память по адресу из `r1`
184: ldr r0, [r1] ; <-- (22) Загружаем значение по адресу из `r1` в `r0`.
186: bic r0, r0, #0x200000 ; <-- (22) Побитовое И значения `r0` с побитовой инверсией `0x200000`, результат сохраняем в `r0`
18a: str r0, [r1] ; <-- (22) Записываем содержимое `r0` в память по адресу из `r1`
18c: ldr r0, [r1] ; <-- (25) Загружаем значение по адресу из `r1` в `r0`.
18e: bic r0, r0, #0x80000 ; <-- (25) Побитовое И значения `r0` с побитовой инверсией `0x80000`, результат сохраняем в `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`, результат сохраняем в `r1`
176: str r1, [r0] ; <-- (?) Записываем содержимое `r1` в память по адресу из `r0`
178: b 0x178 <registers::__cortex_m_rt_main::h1f38525e07b97485+0x18> @ imm = #-0x4
[...]
Хм, всего одна загрузка, одна битовая операция и одна запись? Состояние светодиодов в этот раз не изменилось! Инструкция str — та, что записывает значение в регистр. В отладочной (неоптимизированной) программе их было четыре, по одной на каждую запись в регистр, а в release (оптимизированной) программе только одна.
Как не дать LLVM ошибочно оптимизировать программу? Вместо обычных чтений и записей используем volatile-операции (examples/volatile.rs):
#![no_main]
#![no_std]
use core::ptr;
#[allow(unused_imports)]
use registers::entry;
#[entry]
fn main() -> ! {
registers::init();
unsafe {
// «Магический» адрес!
const PORT_P0_OUT: u32 = 0x50000504;
// Включаем верхний ряд
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out | 1 << 21);
// Включаем нижний ряд
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out | 1 << 19);
// Выключаем верхний ряд
let out = ptr::read_volatile(PORT_P0_OUT as *mut u32);
ptr::write_volatile(PORT_P0_OUT as *mut u32, out & !(1 << 21));
// Выключаем нижний ряд
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
[...]
Смотрите-ка! Теперь у нас снова четыре цикла «загрузка — изменение — запись». Пройдите код ещё раз в GDB, чтобы увидеть volatile-операции в действии!