Почему a*b/c вместо a*(b/c) дает в 3 раза больший размер программы на AVR?

Недавно я попытался упаковать свой код в небольшой 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 добавляет так много кода в первом примере?

Другой вопрос, почему компилятор не встраивает правую часть уравнения, если оба числа известны во время компиляции.


person aso    schedule 10.06.2020    source источник
comment
Эти два выражения не совпадают. Если F_CPU является константой времени компиляции, то F_CPU / 58000 также является константой времени компиляции, что приводит к одному умножению.   -  person Groo    schedule 10.06.2020
comment
Что такое F_CPU? Константа или переменная или что-то еще?   -  person Sami Kuhmonen    schedule 10.06.2020
comment
F_CPU является константой, определенной в начале файла или переключателем -DF_CPU=... для avr-gcc.   -  person aso    schedule 10.06.2020
comment
Каково значение F_CPU?   -  person chux - Reinstate Monica    schedule 10.06.2020
comment
Это unsigned long, в этом примере я использовал 1200000, переданный через флаг компилятору.   -  person aso    schedule 10.06.2020
comment
Спасибо УФ. Каково значение TCNT0?   -  person chux - Reinstate Monica    schedule 10.06.2020
comment
@aso, пожалуйста, укажите всю соответствующую информацию в вопросе, особенно значения TCNT0 и F_CPU и то, как именно они определены, что вызывает проблему.   -  person Jabberwocky    schedule 10.06.2020
comment
@Jabberwocky они специфичны для AVR, например, F_CPU определяется во время компиляции (зависит от тактовой частоты целевого ЦП), а TCNT0 - это регистр таймера.   -  person aso    schedule 10.06.2020
comment
@aso, но, пожалуйста, покажите конкретные значения, которые вызывают проблему. Изменить вопрос.   -  person Jabberwocky    schedule 10.06.2020
comment
@aso: предполагая, что этот процессор 32-разрядный, первое выражение переполнится, если TCNT0 больше 1789. Это предполагает, что TCNT0 является 16-разрядным и будет повышен до 32-разрядного целого числа со знаком. Второе выражение может работать корректно, если вас не беспокоит тот факт, что 1200000/58000 не является хорошим круглым целым числом.   -  person Groo    schedule 10.06.2020
comment
TCNT0 = TCNT0 * F_CPU / 58000 и TCNT0 = (TCNT0 * F_CPU) / 58000 — два совершенно разных выражения.   -  person Jabberwocky    schedule 10.06.2020
comment
@Jabberwocky Прежде всего, выражение со скобками равно TCNT0 * (F_CPU / 58000), а F_CPU известен во время компиляции. Вопрос в том, почему компилятор не встраивает часть этого уравнения, а если не может - зачем добавляет столько ассемблерного кода? Нужен ли процессорам AVR специальный код для таких базовых математических вычислений? Я знаю, что они 8-битные, но все равно - объем генерируемого кода огромен.   -  person aso    schedule 10.06.2020
comment
@Groo, насколько результат второго выражения будет отличаться от первого? 1, так как целое число сокращает часть с плавающей запятой вместо округления или больше, в зависимости от значений?   -  person aso    schedule 10.06.2020
comment
@aso: язык программирования C довольно ясен в отношении порядка операций и требуемого целочисленного продвижения. ATTin13 — это 8-битный контроллер, а это означает, что 32-битное деление займет много циклов. И вы форсируете 32-битное деление, умножая значение на F_CPU.   -  person Groo    schedule 10.06.2020
comment
@также вы не показали ассемблерный код для обеих версий. И каково значение F_CPU в вашем случае?   -  person Jabberwocky    schedule 10.06.2020
comment
@Groo: AVR - это 8-битный RISC-микроконтроллер ISA. Регистров много, но по 8 бит каждый. Вот почему требуется несколько регистров для передачи 16-битных int или 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.2020
comment
@aso: попробуй сам, floor(50*1200000/58000) == 1034, но 50*floor(1200000/58000) == 1000.   -  person Groo    schedule 10.06.2020
comment
@PeterCordes так что, насколько я понял, если я заключу правую часть в скобки, компилятор вставит результат, который даст 8-битное значение, а умножение будет выполняться на двух 8-битных регистрах, но если скобок нет, все значения будут обрабатываться как 16- или 32-битные значения, и поэтому генерируется дополнительный код, потому что 8-битный ЦП не может легко выполнять операции mul/div над 8-битными переменными. Верно?   -  person aso    schedule 10.06.2020
comment
val *= (F_CPU / 58000) встраивает умножение как некоторые инструкции сдвига и сложения и оптимизирует его до 8-битной точности, поскольку вы используете только 8 бит конечного результата. Но с другой стороны, есть 32-битные входные данные для деления, и результат деления действительно зависит от старших битов входных данных. Таким образом, он не может усекаться раньше и должен соблюдать правила целочисленного продвижения C. (т. е. 1200000 достаточно велико, чтобы иметь тип long, поэтому результатом будет long.)   -  person Peter Cordes    schedule 10.06.2020
comment
Кроме того, опубликуйте значение и тип TCNT0 при выполнении TCNT0 * F_CPU / 58000.   -  person chux - Reinstate Monica    schedule 10.06.2020
comment
AVR — очень простой процессор. Он не знает, как умножать или делить. Вы должны написать функцию для умножения и деления чисел на этом процессоре! К счастью, компилятор может сделать это за вас. Компиляторы уже написали функцию, и компилятор просто добавляет ее в ваш код, когда это необходимо.   -  person user253751    schedule 10.06.2020


Ответы (1)


У нас есть это:

x = x * 1200000 / 58000

Обратите внимание, что 1200000/58000 = 20.69... не является целым числом, поэтому его нужно сначала вычислить как умножение, а затем деление на пол. В вашей архитектуре нет собственного целочисленного деления для этого типа данных, поэтому она должна его эмулировать, что приводит к большому количеству кода.

Однако это:

x = x * (1200000 / 58000)

мы находим, что 1200000 / 58000 = 20, поскольку C использует деление пола, поэтому этот код упрощается до простого:

x = x * 20
person orlp    schedule 10.06.2020
comment
... и x *= 20 достаточно прост, чтобы компилятор мог встроить некоторые сдвиги и добавления, поэтому вспомогательные функции libgcc не должны быть связаны с двоичным файлом. И ему нужно только вычислить его с 8-битной шириной, никогда не вычисляя старшую половину результата int, потому что он знает, что он будет усечен до 8 бит. (Но результаты деления зависят от старших битов входных данных, поэтому такая оптимизация невозможна). godbolt.org/z/JF9wq2 показывает оптимизированный asm для AVR gcc9.2 -O3 -mmcu=attiny13. - person Peter Cordes; 10.06.2020
comment
так как C использует деление полов --› не совсем. Начиная с C99, при делении дробь усекается (в направлении 0), а не уменьшается (в направлении -inf). То же самое, когда частное положительное, но другое, когда отрицательное. IAC, OP сообщает, что F_CPU имеет длину без знака, поэтому деление, безусловно, положительное. - person chux - Reinstate Monica; 10.06.2020