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

Ошибочная оптимизация

Чтения и записи в регистры — особенная вещь. Осмелюсь даже сказать, что они воплощают побочные эффекты. В предыдущем примере мы четыре раза записали разные значения в один и тот же регистр. Если бы вы не знали, что этот адрес — регистр, вы могли бы упростить логику и просто записать в регистр итоговое значение 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-операции в действии!