Как предотвратить оптимизацию GCC цикла ожидания занятости?

Я хочу написать прошивку с кодом C для микроконтроллеров Atmel AVR. Я скомпилирую его с помощью GCC. Кроме того, я хочу включить оптимизацию компилятора ( -Os или -O2 ), так как я не вижу причин не включать их, и они, вероятно, сгенерируют лучшую сборку быстрее, чем написание сборка вручную.

Но мне нужен небольшой фрагмент кода, не оптимизированный. Я хочу отложить выполнение функции на некоторое время, и поэтому я хотел написать цикл бездействия, просто чтобы потратить немного времени. Не нужно быть точным, просто подождите некоторое время.

/* How to NOT optimize this, while optimizing other code? */
unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i);
}

Поскольку доступ к памяти в AVR намного медленнее, я хочу, чтобы i и j хранились в регистрах процессора.


] Обновление: я только что нашел util / delay.h и util / delay_basic.h из AVR Libc . Хотя в большинстве случаев было бы лучше использовать эти функции, этот вопрос остается актуальным и интересным.


Связанные вопросы:

62
задан Denilson Sá Maia 18 September 2018 в 03:26
поделиться

1 ответ

операторы Empty __asm__ недостаточно: лучше используйте зависимости по данным

, Например:

Компиляция main.c

int main(void) {
    unsigned i;
    for (i = 0; i < 10; i++) {
        __asm__ volatile("");
    }
}

и демонтируйте:

gcc -O3 -ggdb3 -o main.out main.c
gdb -batch -ex 'disas main' main.out

вывод:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     retq

Также примечание, которое __asm__("") и __asm__ volatile("") должно быть тем же с тех пор, нет никаких выходных операндов: различие между asm, asm энергозависимая и ударяющая память

, Что происходит, становится более ясным, если мы заменяем его:

__asm__ volatile("");

, который производит:

   0x0000000000001040 <+0>:     nop
   0x0000000000001041 <+1>:     nop
   0x0000000000001042 <+2>:     nop
   0x0000000000001043 <+3>:     nop
   0x0000000000001044 <+4>:     nop
   0x0000000000001045 <+5>:     nop
   0x0000000000001046 <+6>:     nop
   0x0000000000001047 <+7>:     nop
   0x0000000000001048 <+8>:     nop
   0x0000000000001049 <+9>:     nop
   0x000000000000104a <+10>:    xor    %eax,%eax
   0x000000000000104c <+12>:    retq

, Таким образом, мы видим, что GCC всего цикл развернул nop цикл в этом случае, потому что цикл был достаточно маленьким.

Так, при использовании этой техники Вы полагались бы трудно для предсказания двоичных компромиссов размера/скорости GCC, которые, если бы применено оптимально, должен всегда удалять цикл для пустого __asm__ volatile("");, который имеет нуль размера кода.

Одно решение, которому я верю, надежно, должен заменить его явной зависимостью по данным от переменной цикла i, как предложено в: порядок оператора Enforcing в C++

__asm__ ("" : "+g" (i) : :);

, который производит желаемый цикл:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     nopw   0x0(%rax,%rax,1)
   0x0000000000001048 <+8>:     add    [116]x1,%eax
   0x000000000000104b <+11>:    cmp    [116]x9,%eax
   0x000000000000104e <+14>:    jbe    0x1048 <main+8>
   0x0000000000001050 <+16>:    xor    %eax,%eax
   0x0000000000001052 <+18>:    retq 

Это отмечает i как ввод и вывод встроенного ассемблерного кода. Затем встроенный ассемблерный код является черным квадратом для GCC, который не может знать, как он изменяет i, таким образом, я думаю, что действительно не может быть оптимизирован далеко.

noinline занятая функция цикла

, Если размер цикла не известен во время компиляции, полное разворачивание не возможно, но GCC мог все еще решить развернуть в блоках, которые сделают Ваши задержки непоследовательными.

Помещение, что вместе с [1 123] ответ Denilson , я пошел бы для:

void __attribute__ ((noinline)) busy_loop(unsigned max) {
    for (unsigned i = 0; i < max; i++) {
        __asm__ volatile("" : "+g" (i) : :);
    }
}

int main(void) {
    busy_loop(10);
}

, который демонтирует в:

Dump of assembler code for function busy_loop:
   0x0000000000001140 <+0>:     test   %edi,%edi
   0x0000000000001142 <+2>:     je     0x1157 <busy_loop+23>
   0x0000000000001144 <+4>:     xor    %eax,%eax
   0x0000000000001146 <+6>:     nopw   %cs:0x0(%rax,%rax,1)
   0x0000000000001150 <+16>:    add    [118]x1,%eax
   0x0000000000001153 <+19>:    cmp    %eax,%edi
   0x0000000000001155 <+21>:    ja     0x1150 <busy_loop+16>
   0x0000000000001157 <+23>:    retq   
End of assembler dump.
Dump of assembler code for function main:
   0x0000000000001040 <+0>:     mov    [118]xa,%edi
   0x0000000000001045 <+5>:     callq  0x1140 <busy_loop>
   0x000000000000104a <+10>:    xor    %eax,%eax
   0x000000000000104c <+12>:    retq   
End of assembler dump.

Здесь эти volatile, был необходим для маркировки блока как потенциальное наличие побочных эффектов, так как в этом случае у нас есть выходные переменные.

А двойная версия цикла могла быть:

void __attribute__ ((noinline)) busy_loop(unsigned max, unsigned max2) {
    for (unsigned i = 0; i < max; i++) {
        for (unsigned j = 0; j < max2; j++) {
            __asm__ volatile ("" : "+g" (j), "+g" (j) : :);
        }
    }
}

int main(void) {
    busy_loop(10, 10);
}

GitHub в восходящем направлении .

Связанные потоки:

Протестированный в Ubuntu 19.04, GCC 8.3.0.

1
ответ дан 24 November 2019 в 16:47
поделиться
Другие вопросы по тегам:

Похожие вопросы: