Недавно я попытался упаковать свой код в небольшой ATTiny13 с 1 КБ флэш-памяти. В процессе оптимизации я обнаружил нечто странное для себя. Возьмем пример кода:
#include <avr/interrupt.h>
int main() {
TCNT0 = TCNT0 * F_CPU / 58000;
}
Смысла в этом нет конечно, но интересен размер вывода - он выдает 248 байт.
Краткое объяснение кода: F_CPU
— константа, определяемая переключателем -DF_CPU=...
для avr-gcc
, TCNT0
— 8-битный регистр (на ATTiny13). В реальной программе я присваиваю результат уравнения uint16_t, но все равно наблюдается такое же поведение.
Если бы часть выражения была заключена в скобки:
TCNT0 = TCNT0 * (F_CPU / 58000);
Размер выходного файла составляет 70 байт. Огромная разница, но результаты этих операций одинаковы (верно?).
Я просмотрел сгенерированный ассемблерный код и, несмотря на то, что я не очень хорошо понимаю ASM, я вижу, что версия без скобок добавляет некоторые метки, такие как:
00000078 <__divmodsi4>:
78: 05 2e mov r0, r21
7a: 97 fb bst r25, 7
7c: 16 f4 brtc .+4 ; 0x82 <__divmodsi4+0xa>
7e: 00 94 com r0
80: 0f d0 rcall .+30 ; 0xa0 <__negsi2>
82: 57 fd sbrc r21, 7
84: 05 d0 rcall .+10 ; 0x90 <__divmodsi4_neg2>
86: 14 d0 rcall .+40 ; 0xb0 <__udivmodsi4>
88: 07 fc sbrc r0, 7
8a: 02 d0 rcall .+4 ; 0x90 <__divmodsi4_neg2>
8c: 46 f4 brtc .+16 ; 0x9e <__divmodsi4_exit>
8e: 08 c0 rjmp .+16 ; 0xa0 <__negsi2>
И многое другое. Я какое-то время выучил только ассемблер x86, но насколько я помню, для деления была простая мнемоника. Почему avr-gcc
добавляет так много кода в первом примере?
Другой вопрос, почему компилятор не встраивает правую часть уравнения, если оба числа известны во время компиляции.
F_CPU
является константой времени компиляции, тоF_CPU / 58000
также является константой времени компиляции, что приводит к одному умножению. - person Groo   schedule 10.06.2020F_CPU
? Константа или переменная или что-то еще? - person Sami Kuhmonen   schedule 10.06.2020F_CPU
является константой, определенной в начале файла или переключателем-DF_CPU=...
для avr-gcc. - person aso   schedule 10.06.2020F_CPU
? - person chux - Reinstate Monica   schedule 10.06.2020TCNT0
? - person chux - Reinstate Monica   schedule 10.06.2020TCNT0
иF_CPU
и то, как именно они определены, что вызывает проблему. - person Jabberwocky   schedule 10.06.2020TCNT0
больше 1789. Это предполагает, чтоTCNT0
является 16-разрядным и будет повышен до 32-разрядного целого числа со знаком. Второе выражение может работать корректно, если вас не беспокоит тот факт, что1200000/58000
не является хорошим круглым целым числом. - person Groo   schedule 10.06.2020TCNT0 = TCNT0 * F_CPU / 58000
иTCNT0 = (TCNT0 * F_CPU) / 58000
— два совершенно разных выражения. - person Jabberwocky   schedule 10.06.2020TCNT0 * (F_CPU / 58000)
, а F_CPU известен во время компиляции. Вопрос в том, почему компилятор не встраивает часть этого уравнения, а если не может - зачем добавляет столько ассемблерного кода? Нужен ли процессорам AVR специальный код для таких базовых математических вычислений? Я знаю, что они 8-битные, но все равно - объем генерируемого кода огромен. - person aso   schedule 10.06.2020ATTin13
— это 8-битный контроллер, а это означает, что 32-битное деление займет много циклов. И вы форсируете 32-битное деление, умножая значение наF_CPU
. - person Groo   schedule 10.06.2020F_CPU
в вашем случае? - person Jabberwocky   schedule 10.06.2020int
или 32-битныхlong
аргументов вспомогательным функциям mul и div. (int
— это 16-битный минимум, который требуется C, но1200000
имеет типlong
, потому что он слишком велик дляint
.) В Godbolt установлен gcc для AVR. godbolt.org/z/JF9wq2 показывает оптимизированный ассемблер для-O3 -mmcu=attiny13
. (Он встраивает умножение как сдвиг/сложение в случаеfoo *= (x/y)
с небольшим постоянным множителем времени компиляции.) - person Peter Cordes   schedule 10.06.2020floor(50*1200000/58000) == 1034
, но50*floor(1200000/58000) == 1000
. - person Groo   schedule 10.06.2020val *= (F_CPU / 58000)
встраивает умножение как некоторые инструкции сдвига и сложения и оптимизирует его до 8-битной точности, поскольку вы используете только 8 бит конечного результата. Но с другой стороны, есть 32-битные входные данные для деления, и результат деления действительно зависит от старших битов входных данных. Таким образом, он не может усекаться раньше и должен соблюдать правила целочисленного продвижения C. (т. е.1200000
достаточно велико, чтобы иметь типlong
, поэтому результатом будетlong
.) - person Peter Cordes   schedule 10.06.2020TCNT0
при выполненииTCNT0 * F_CPU / 58000
. - person chux - Reinstate Monica   schedule 10.06.2020