Как предотвратить 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. Хотя в большинстве случаев лучше использовать эти функции, этот вопрос остается актуальным и интересным.
Смежные вопросы:
8 ответов
Я разработал этот ответ после перехода по ссылке из ответа dmckee, но он использует иной подход, чем его / ее ответ.
Документация по атрибутам функций из GCC упоминает:
noinline
Этот атрибут функции не позволяет рассматривать функцию для встраивания. Если функция не имеет побочных эффектов, есть другие оптимизации, кроме встраивания, которые вызывают оптимизацию вызовов функций, хотя вызов функции является живым. Чтобы такие звонки не оптимизировались, поставьтеasm ("");
Это дало мне интересную идею... Вместо добавления nop
Инструкция во внутреннем цикле, я попытался добавить туда пустой код сборки, например:
unsigned char i, j;
j = 0;
while(--j) {
i = 0;
while(--i)
asm("");
}
И это сработало! Этот цикл не был оптимизирован, и никаких дополнительных nop
инструкции были вставлены.
Более того, если вы используете volatile
GCC будет хранить эти переменные в оперативной памяти и добавить кучу ldd
а также std
скопировать их во временные регистры. Этот подход, с другой стороны, не использует volatile
и не генерирует такие накладные расходы.
Обновление: если вы компилируете код, используя -ansi
или же -std
, вы должны заменить asm
ключевое слово с __asm__
, как описано в документации GCC.
Кроме того, вы также можете использовать __asm__ __volatile__("")
если ваш оператор сборки должен выполняться там, где мы его поместили (т.е. не должен быть перемещен из цикла в качестве оптимизации).
Декларировать i
а также j
переменные как volatile
, Это не позволит компилятору оптимизировать код, включающий эти переменные.
unsigned volatile char i, j;
Пустой __asm__
заявлений недостаточно: лучше использовать зависимости данных
Нравится:
main.c
int main(void) {
unsigned i;
for (i = 0; i < 10; i++) {
__asm__ volatile("" : "+g" (i) : :);
}
}
Скомпилировать и разобрать:
gcc -O3 -ggdb3 -o main.out main.c
gdb -batch -ex 'disas main' main.out
Выход:
0x0000000000001040 <+0>: xor %eax,%eax
0x0000000000001042 <+2>: nopw 0x0(%rax,%rax,1)
0x0000000000001048 <+8>: add $0x1,%eax
0x000000000000104b <+11>: cmp $0x9,%eax
0x000000000000104e <+14>: jbe 0x1048 <main+8>
0x0000000000001050 <+16>: xor %eax,%eax
0x0000000000001052 <+18>: retq
Я считаю, что это надежно, потому что оно устанавливает явную зависимость данных от переменной цикла i
как предлагается в: Обеспечение порядка операторов в C++ и создание желаемого цикла:
Это отмечает i
как вход и выход встроенной сборки. Тогда встроенная сборка - это черный ящик для GCC, который не может знать, как он изменяетi
, поэтому я думаю, что это действительно невозможно оптимизировать.
Если я сделаю то же самое с пустым __asm__
как в:
bad.c
int main(void) {
unsigned i;
for (i = 0; i < 10; i++) {
__asm__ volatile("");
}
}
похоже, полностью удаляет цикл и выводит:
0x0000000000001040 <+0>: xor %eax,%eax
0x0000000000001042 <+2>: retq
Также обратите внимание, что __asm__("")
а также __asm__ volatile("")
должно быть таким же, поскольку нет выходных операндов: разница между asm, asm volatile и clobbering memory
То, что происходит, станет яснее, если мы заменим это на:
__asm__ volatile("nop");
который производит:
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
в данном случае петля, потому что петля была достаточно маленькой.
Итак, если вы полагаетесь на пустой __asm__
, вы бы полагались на трудно предсказуемые компромиссы между размером и скоростью двоичного кода GCC, которые при оптимальном применении всегда должны удалять цикл для пустого __asm__ volatile("");
который имеет нулевой размер кода.
noinline
функция цикла занятости
Если размер цикла неизвестен во время компиляции, полная развертка невозможна, но GCC все равно может решить развернуть ее по частям, что сделает ваши задержки непоследовательными.
Объединяя это вместе с ответом Денилсона, функция цикла занятости может быть записана как:
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 $0x1,%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 $0xa,%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 < max2; i++) {
for (unsigned j = 0; j < max; j++) {
__asm__ volatile ("" : "+g" (i), "+g" (j) : :);
}
}
}
int main(void) {
busy_loop(10, 10);
}
Связанные темы:
Протестировано в Ubuntu 19.04, GCC 8.3.0.
Я не уверен, почему еще не было упомянуто, что этот подход полностью ошибочен и легко нарушается при обновлении компилятора и т. Д. Было бы гораздо разумнее определить значение времени, которое вы хотите дождаться, и прокрутить текущий опрос. время, пока желаемое значение не будет превышено. На x86 вы можете использовать rdtsc
для этой цели, но более портативным способом было бы позвонить clock_gettime
(или вариант для вашей не-POSIX ОС), чтобы получить время. Текущий x86_64 Linux будет даже избегать системного вызова для clock_gettime
и использовать rdtsc
внутренне. Или, если вы можете справиться со стоимостью системного вызова, просто используйте clock_nanosleep
начать с...
Для меня в GCC 4.7.0 пустой asm был оптимизирован в любом случае с -O3 (не пытался с -O2). и использование i++ в регистре или volatile привело к значительному снижению производительности (в моем случае).
Я связался с другой пустой функцией, которую компилятор не мог видеть при компиляции "основной программы".
В основном это:
Создан "helper.c" с объявленной функцией (пустая функция)
void donotoptimize(){}
Затем скомпилировал "gcc helper.c -c -o helper.o" и затем
while (...) { donotoptimize();}
Это дало мне лучшие результаты (и, по моему убеждению, никаких накладных расходов, но я не могу проверить, потому что моя программа не будет работать без него:))
Я думаю, что это должно работать с ICC тоже. Может быть, нет, если вы включаете оптимизацию компоновки, но с помощью gcc это происходит.
Я не знаю, из головы, если AVR-версия компилятора поддерживает полный набор #pragma
s (все интересные ссылки приведены в gcc версии 4.4), но это то, с чего вы обычно начинаете.
Поместите этот цикл в отдельный файл.c и не оптимизируйте этот файл. Еще лучше написать эту подпрограмму на ассемблере и вызвать ее из C, в любом случае оптимизатор не вмешается.
Иногда я делаю изменчивую вещь, но обычно создаю функцию asm, которая просто возвращает вызов функции, оптимизатор сделает цикл for/while плотным, но не оптимизирует его, потому что он должен выполнять все вызовы фиктивной функции. Ноп ответ от Денилсона Са делает то же самое, но даже крепче...
Помещение летучего асма должно помочь. Вы можете прочитать больше об этом здесь: -
http://www.nongnu.org/avr-libc/user-manual/optimization.html
Если вы работаете в Windows, вы можете даже попробовать поместить код под прагмы, как подробно описано ниже:
Надеюсь это поможет.
Вы также можете использовать ключевое словорегистра. Переменные, объявленные с помощью регистра, хранятся в регистрах процессора.
В твоем случае:
register unsigned char i, j;
j = 0;
while(--j) {
i = 0;
while(--i);
}