Насыщающая оптимизация добавления в avr-gcc

Я программирую для 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;
}

Но похоже, что это может сломаться из-за переупорядочения кода компилятора? Но мы не можем сделать блок asmvolatile, потому что это отключает еще больше оптимизации.

Итак, мои вопросы таковы:

Кто-нибудь смог заставить avr-gcc правильно оптимизировать это без встроенной сборки?
Есть ли правильный способ оптимизировать его с помощью встроенной сборки, чтобы он оптимизировал константы?

Вы используете неправильные ограничения во встроенном ассемблере LDI, обязательном d, то же самое для SUBI, что лучше всего подходит при добавлении констант.

emacs drives me nuts 30.07.2024 15:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
55
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы можете использовать встроенный 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. Спасибо за ответ, я не ненавижу вас, просто ненавидю компилятор.

Kryštof Vosyka 30.07.2024 05:07

Обратите внимание, что __builtin_constant_p разрешается во время компиляции, а не выполнения. Либо компилятор может «доказать», что оно константно, либо нет. Так что да, у вас будет несколько блоков, но вы не платите за if во время выполнения.

David Wohlferd 30.07.2024 06:14

Некоторые ограничения неверны. LDI требует d.

emacs drives me nuts 30.07.2024 15:11
Ответ принят как подходящий

Кто-нибудь смог заставить 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, поскольку он вычитает отрицательное значение, хотя на самом деле он реализуется для сложения положительного значения.

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

Kryštof Vosyka 30.07.2024 17:33

Насколько мне известно, avr-gcc не делает ничего особенного (например, оптимизации) с __builtin_xxx_overflow, возможно, некоторые байты можно выжать, предоставив соответствующие шаблоны.

emacs drives me nuts 30.07.2024 17:51

Другие вопросы по теме