Я программирую для ATtiny13 и мне приходится делать много насыщенных дополнений.
Пытаясь их оптимизировать, создается впечатление, что avr-gcc вообще ничего не умеет оптимизировать. Все это было опробовано с AVR gcc 14.1.0 с помощью -O3
. Вот что я пробовал до сих пор:
uint8_t saturating_add_1(uint8_t a, uint8_t b) {
uint8_t temp = a + b;
if (temp < a)
return 0xFF;
return temp;
}
Это успешно оптимизируется на x86, однако avr-gcc дает нам следующее:
saturating_add_1:
.L__stack_usage = 0
mov r25,r24
add r24,r22
cp r24,r25
brlo .L1
ret
.L1:
ldi r24,lo8(-1)
ret
Не здорово, не ужасно, он делает то, что мы ему сказали. Давайте попробуем другую версию, которая, как известно, правильно оптимизируется на других архитектурах:
uint8_t saturating_add_2(uint8_t a, uint8_t b) {
if (b > 255 - a)
return 255;
else return a + b;
}
Нет, это еще хуже:
saturating_add_2:
.L__stack_usage = 0
ldi r18,lo8(-1)
ldi r19,0
sub r18,r24
sbc r19,__zero_reg__
cp r18,r22
cpc r19,__zero_reg__
brlt .L1
add r24,r22
ret
.L1:
ldi r24,lo8(-1)
ret
Хорошо, я думаю, мы пробуем встроенные функции компилятора.
uint8_t saturating_add_builtin(uint8_t a, uint8_t b) {
if (__builtin_add_overflow(a, b, &a))
return 255;
else return a;
}
saturating_add_builtin:
.L__stack_usage = 0
add r22,r24
cp r22,r24
brlo .L1
mov r24,r22
ret
.L1:
ldi r24,lo8(-1)
ret
Он генерирует более или менее ту же сборку, что и наша первая попытка. Я ожидаю, что он не будет сравниваться, а будет использовать инструкцию brcs
или brcc
(ветвь, если перенос установлен/очистен).
Может быть, мы можем заставить его?
uint8_t saturating_add_reg(uint8_t a, uint8_t b) {
uint8_t temp = a + b;
if (SREG & 1)
return 255;
return temp;
}
saturating_add_reg:
.L__stack_usage = 0
add r24,r22
in __tmp_reg__,__SREG__
sbrs __tmp_reg__,0
ret
ldi r24,lo8(-1)
ret
}
Это несколько лучше, с 7 инструкций до 6. Но avr-gcc снова сбивает меня с толку: почему он использует sbrs
для пропуска ret
вместо sbrc
для пропуска ldi
? Я что-то пропустил?
В любом случае, я также пытался исправить это с помощью встроенного ассемблера, однако это немного громоздко:
uint8_t saturating_add_asm_1(uint8_t a, uint8_t b) {
asm (
"add %[a], %[b]\n\t"
"brcc no_overflow_%=\n\t"
"ldi %[a], 255\n\t"
"no_overflow_%=:"
: [a] "+r" (a)
: [b] "r" (b)
: "cc"
);
return a;
}
Это прекрасно работает, но компилятор не может оптимизировать константы (с subi
), что после всего времени, которое я потратил на это, причиняет боль на эмоциональном уровне. Моя другая попытка:
uint8_t saturating_add_asm_2(uint8_t a, uint8_t b) {
uint8_t temp = a + b;
asm (
"brcc no_overflow_%=\n\t"
"ldi %[temp], 255\n\t"
"no_overflow_%=:"
: [temp] "+r" (temp)
:
:
);
return temp;
}
Но похоже, что это может сломаться из-за переупорядочения кода компилятора? Но мы не можем сделать блок asm
volatile
, потому что это отключает еще больше оптимизации.
Итак, мои вопросы таковы:
Кто-нибудь смог заставить avr-gcc правильно оптимизировать это без встроенной сборки?
Есть ли правильный способ оптимизировать его с помощью встроенной сборки, чтобы он оптимизировал константы?
Вы можете использовать встроенный gcc __builtin_constant_p
для возврата к реализации без asm для постоянного случая:
uint8_t saturating_add_non_asm(uint8_t a, uint8_t b) {
uint8_t temp = a + b;
if (temp < a)
return 0xFF;
return temp;
}
uint8_t saturating_add(uint8_t a, uint8_t b) {
if (__builtin_constant_p(a) && __builtin_constant_p(b)) {
return saturating_add_non_asm(a, b);
}
asm (
"add %[a], %[b]\n\t"
"brcc no_overflow_%=\n\t"
"ldi %[a], 255\n\t"
"no_overflow_%=:"
: [a] "+r" (a)
: [b] "r" (b)
: "cc"
);
return a;
}
Что ж, это ужасно, таким образом мне пришлось бы проверять их обе на постоянство, иметь четыре разные ветки в зависимости от того, являются ли a
, b
или обе константными, и иметь для каждой разные блоки asm. Спасибо за ответ, я не ненавижу вас, просто ненавидю компилятор.
Обратите внимание, что __builtin_constant_p
разрешается во время компиляции, а не выполнения. Либо компилятор может «доказать», что оно константно, либо нет. Так что да, у вас будет несколько блоков, но вы не платите за if
во время выполнения.
Некоторые ограничения неверны. LDI
требует d
.
Кто-нибудь смог заставить avr-gcc правильно оптимизировать это без встроенной сборки?
Да. Кажется, лучший код (и самый простой) код, который вы получаете с помощью поддержки фиксированной точки согласно ISO/IEC TR 18037 «Встроенный C»:
#include <stdint.h>
#include <stdfix.h>
static inline __attribute__((always_inline))
uint8_t addu8_sat (uint8_t a, uint8_t b)
{
unsigned short sat fract ha = uhrbits (a);
unsigned short sat fract hb = uhrbits (b);
return bitsuhr (ha + hb);
}
static inline __attribute__((always_inline))
int8_t addi8_sat (int8_t a, int8_t b)
{
short sat fract ha = hrbits (a);
short sat fract hb = hrbits (b);
return bitshr (ha + hb);
}
uint8_t test_u8 (uint8_t a, uint8_t b)
{
return addu8_sat (addu8_sat (a, b), 10);
}
int8_t test_i8 (int8_t a, int8_t b)
{
return addi8_sat (addi8_sat (a, b), 10);
}
Давайте посмотрим на ассемблерный код, сгенерированный с помощью
$ avr-gcc -Os sat.c -std=gnu99 -S -dp
test_u8:
add r24,r22 ; 9 [c=4 l=3] usadduqq3/0
brcc 0f
sbc r24,r24
0:
subi r24,-10 ; 18 [c=4 l=3] usadduqq3/1
brcs 0f
ldi r24,0xff
0:
ret ; 24 [c=0 l=1] return
test_i8:
add r24,r22 ; 9 [c=4 l=5] ssaddqq3/0
brvc 0f
ldi r24,0x80
sbrs r22,7
dec r24
0:
subi r24,-10 ; 18 [c=4 l=3] ssaddqq3/1
brvc 0f
ldi r24,0x7f
0:
ret ; 24 [c=0 l=1] return
Таким образом, он использует 3 инструкции, за исключением случая со знаком и когда ни одно дополнение не известно во время компиляции. Хитрость заключается в том, что в этом случае насыщенность зависит от знака слагаемого, который неизвестен.
При этом используется тот факт, что сложения Q-формата являются двоичными, как и сложения целых чисел, поэтому реализации в основном связаны с переводом между двумя мирами. Обратите внимание, что это не работает для умножения или деления. А смешивать знаковые и беззнаковые сложнее, поскольку они не коммутируют.
Поддержка фиксированной точки была добавлена в avr-gcc v4.8 как часть GNU-C99.
К вашему сведению, вот соответствующий код для вычитания:
static inline __attribute__((always_inline))
uint8_t subu8_sat (uint8_t a, uint8_t b)
{
unsigned short sat fract ha = uhrbits (a);
unsigned short sat fract hb = uhrbits (b);
return bitsuhr (ha - hb);
}
static inline __attribute__((always_inline))
int8_t subi8_sat (int8_t a, int8_t b)
{
short sat fract ha = hrbits (a);
short sat fract hb = hrbits (b);
return bitshr (ha - hb);
}
uint8_t test_sub_u8 (uint8_t a, uint8_t b)
{
return subu8_sat (subu8_sat (a, b), 10);
}
int8_t test_sub_i8 (int8_t a, int8_t b)
{
return subi8_sat (subi8_sat (a, b), 10);
}
test_sub_u8:
sub r24,r22 ; 9 [c=4 l=3] ussubuqq3/0
brcc 0f
clr r24
0:
subi r24,10 ; 18 [c=4 l=3] ussubuqq3/1
brcc 0f
clr r24
0:
ret
test_sub_i8:
sub r24,r22 ; 9 [c=4 l=5] sssubqq3/0
brvc 0f
ldi r24,0x7f
sbrs r22,7
inc r24
0:
subi r24,10 ; 18 [c=4 l=3] sssubqq3/1
brvc 0f
ldi r24,0x80
0:
ret
И последнее замечание: случай с константой немного сложен, поскольку прибавление 128 — это не то же самое, что вычитание -128, поэтому необходимо соблюдать особую осторожность.
Кроме того, в случае беззнаковой константы используется обратный флаг C, поскольку он вычитает отрицательное значение, хотя на самом деле он реализуется для сложения положительного значения.
Это далеко за пределами всего, о чем я мог подумать, спасибо. Мне кажется интересным, что каким-то образом операции с фиксированной точкой оптимизируются лучше, чем встроенные.
Насколько мне известно, avr-gcc не делает ничего особенного (например, оптимизации) с __builtin_xxx_overflow
, возможно, некоторые байты можно выжать, предоставив соответствующие шаблоны.
Вы используете неправильные ограничения во встроенном ассемблере
LDI
, обязательномd
, то же самое дляSUBI
, что лучше всего подходит при добавлении констант.