avr-gcc: (на первый взгляд) ненужный пролог/эпилог в простой функции

При попытке адресации отдельных байтов внутри uint64 AVR gcc⁽¹⁾ выдает мне странный пролог/эпилог, в то время как та же функция, написанная с использованием uint32_t, дает мне один ret (пример функции — это NOP).

Почему gcc это делает? Как это удалить?

Вы можете увидеть код здесь, в Compiler Explorer.

⁽¹⁾ gcc 5.4.0 из дистрибутива Arduino 1.8.9, параметры=-O3 -std=c++11.

Исходный код:

#include <stdint.h>

uint32_t f_u32(uint32_t x) {
  union y {
    uint8_t p[4];
    uint32_t w;
  };
  return y{ .p = {
    y{ .w = x }.p[0],
    y{ .w = x }.p[1],
    y{ .w = x }.p[2],
    y{ .w = x }.p[3]
  } }.w;
}

uint64_t f_u64(uint64_t x) {
  union y {
    uint8_t p[8];
    uint64_t w;
  };
  return y{ .p = {
    y{ .w = x }.p[0],
    y{ .w = x }.p[1],
    y{ .w = x }.p[2],
    y{ .w = x }.p[3],
    y{ .w = x }.p[4],
    y{ .w = x }.p[5],
    y{ .w = x }.p[6],
    y{ .w = x }.p[7]
  } }.w;
}

Сгенерированная сборка для uint32_t версии:

f_u32(unsigned long):
  ret

Сгенерированная сборка для версии uint64_t:

f_u64(unsigned long long):
  push r28
  push r29
  in r28,__SP_L__
  in r29,__SP_H__
  subi r28,72
  sbc r29,__zero_reg__
  in __tmp_reg__,__SREG__
  cli
  out __SP_H__,r29
  out __SREG__,__tmp_reg__
  out __SP_L__,r28
  subi r28,-72
  sbci r29,-1
  in __tmp_reg__,__SREG__
  cli
  out __SP_H__,r29
  out __SREG__,__tmp_reg__
  out __SP_L__,r28
  pop r29
  pop r28
  ret

person André Kugland    schedule 04.09.2019    source источник
comment
Где твой вопрос?   -  person David Grayson    schedule 04.09.2019
comment
@DavidGrayson Я добавил это сейчас.   -  person André Kugland    schedule 04.09.2019
comment
Похоже, что какой-то аргумент передает служебные данные, потому что 32-битное целое передается в регистре, но 64-битных регистров нет. Но я не могу сказать наверняка.   -  person Sebastian Redl    schedule 06.09.2019
comment
Функции грамотно оптимизированы, потому что они не используются. В стеке возвращаются только 64-битные значения, поэтому вторая функция выделяет 8 байтов в стеке. Удалите функцию, если хотите удалить это. Полную реализацию функции можно увидеть, если убрать опцию оптимизации.   -  person Juraj    schedule 06.09.2019
comment
Трудно понять, что вы ищете. На ваши вопросы есть тривиальные ответы: (1) gcc делает это, потому что его оптимизатор недостаточно мощен, чтобы уменьшить f_u64() до NOP, и (2) вы можете удалить это, удалив функцию или попытавшись реализовать ее как return x;. Если это не те ответы, которые вы ищете, возможно, вы могли бы перефразировать вопрос или уточнить в комментарии?   -  person nielsen    schedule 07.09.2019
comment
nielsen, это не та функция, которую я хочу написать, это минимальный пример поведения компилятора.   -  person André Kugland    schedule 07.09.2019
comment
это больше, чем минимум, поэтому он не работает как минимум. ты читал мой комментарий?   -  person Juraj    schedule 07.09.2019
comment
Понятно. Это хороший вопрос, но я не думаю, что смогу ответить на него удовлетворительно. Кажется, f_u64() по какой-то причине выделяет 72 байта в стеке, а затем снова их освобождает. Я попытался добавить функцию, которая принимает uint64_t, вызывает f_u64() и возвращает результат плюс 10 и скомпилирована с оптимизацией для size-Os. Эта функция не получает никакой акробатики стека, поэтому это не общий аспект передачи uint64_t. В настоящее время я думаю, что это какая-то проблема с компилятором/оптимизатором, но я не могу указать на это пальцем. Лично я бы смирился с этим или попытался найти обходной путь.   -  person nielsen    schedule 07.09.2019


Ответы (3)


Я не уверен, что это хороший ответ, но это лучшее, что я могу дать. Сборка для функции f_u64() выделяет в стеке 72 байта, а затем снова их освобождает (поскольку при этом задействованы регистры r28 и r29, они сохраняются в начале и восстанавливаются в конце).

Если вы попытаетесь скомпилировать без оптимизации (я также пропустил флаг c++11, я не думаю, что это имеет какое-либо значение), то вы увидите, что функция f_u64() начинается с выделения 80 байтов в стеке (аналогично открывающим операторам, которые вы видите в оптимизированный код, только с 80 байтами вместо 72):

    in r28,__SP_L__
    in r29,__SP_H__
    subi r28,80
    sbc r29,__zero_reg__
    in __tmp_reg__,__SREG__
    cli
    out __SP_H__,r29
    out __SREG__,__tmp_reg__
    out __SP_L__,r28

Фактически все эти 80 байт используются. Сначала сохраняется значение аргумента x (8 байтов), а затем выполняется множество перемещений данных с использованием оставшихся 72 байтов.

После этого 80 байт освобождаются в стеке аналогично закрывающим операторам в оптимизированном коде:

    subi r28,-80
    sbci r29,-1
    in __tmp_reg__,__SREG__
    cli
    out __SP_H__,r29
    out __SREG__,__tmp_reg__
    out __SP_L__,r28

Я предполагаю, что оптимизатор заключает, что 8 байтов для хранения аргумента можно сэкономить. Следовательно, ему нужно всего 72 байта. Затем делается вывод, что все перемещения данных можно избежать. Однако он не может понять, что это означает, что 72 байта в стеке можно сэкономить.

Следовательно, я считаю, что это ограничение или ошибка оптимизатора (как бы вы это ни называли). В этом случае единственное «решение» - попытаться перетасовать реальный код, чтобы найти обходной путь или вызвать его как ошибку в компиляторе.

person nielsen    schedule 09.09.2019
comment
Может быть, причина, по которой компилятор не может это оптимизировать, заключается в наличии инструкции CLI в прологе/эпилоге? - person André Kugland; 12.09.2019
comment
Это вполне может быть тем, что сдерживает его. Я на самом деле озадачен порядком этих утверждений. Кажется разумным отключать прерывания при обновлении SP (указатель стека), но тогда странно восстанавливать SREG (регистр состояния, который включает бит разрешения/отключения прерывания) до завершения обновления SP. - person nielsen; 12.09.2019
comment
На самом деле это оптимизация. Подобно слоту задержки, новый SREG с включенными прерываниями не вступает в силу до следующей инструкции. В техническом описании ATtiny48 это упоминается как «При использовании инструкции SEI для включения прерываний, инструкция, следующая за SEI, будет выполняться перед любыми ожидающими прерываниями, как показано в этом примере». - person Yann Vernier; 13.09.2019
comment
@YannVernier И, по-видимому, то же самое верно и для обновления SREG с помощью out. Спасибо, что разъяснили это. - person nielsen; 13.09.2019
comment
Нашел другое описание поведения обработки отложенных прерываний в AVR-libc. Часто задаваемые вопросы: почему прерывания снова включаются в процессе записи указателя стека? - person Yann Vernier; 14.11.2019

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

Если вы хотите по какой-то причине вызывать эту функцию в другом коде, я бы сделал:

#define f_u64(x) ((uint64_t)(x))
person David Grayson    schedule 06.09.2019
comment
Это не та функция, которую я хочу написать, это минимальный пример поведения компилятора. - person André Kugland; 07.09.2019
comment
Если вы не покажете свой реальный код, то очень сложно ответить на ваш вопрос о том, как избавиться от такого поведения. - person David Grayson; 07.09.2019

Накладные расходы, которые вы видите, являются результатом Endianness того, как ЦП хранит числа. В примере, на который вы ссылаетесь в Compiler Explorer, вы выбрали Uno — этот код GCC генерирует ASM для ATmega328P (с прямым порядком байтов). Вы также сопоставляете uint64 с 8 x uint8, поэтому компилятору необходимо перевернуть старшую и младшую 32-битные части 64-битного числа... а затем вернуть их обратно при возврате. (Вы увидите, что Godbolt показывает две части разными цветами.)

Как это удалить? Именно так работает ATmega328P. Если вы выберете компилятор Raspbain на godbolt, вы увидите, что накладные расходы исчезнут, потому что порядок следования байтов на этой платформе является прямым порядком байтов.

person Vino    schedule 08.09.2019
comment
Не могли бы вы объяснить, как сгенерированный код, который манипулирует указателем стека таким образом, чтобы в стеке выделялось 72 байта, имеет какое-либо отношение к преобразованию байтов? Кроме того, почему преобразование endian необходимо для uint64_t, но не для uint32_t? Извините, но я вообще не понимаю этого ответа. - person nielsen; 09.09.2019