Вопрос

Каков наилучший (самый чистый и эффективный) способ написать насыщающее сложение на C?

Функция или макрос должны добавить два беззнаковых входа (нужны как 16-, так и 32-битные версии) и вернуть все биты — единицу (0xFFFF или 0xFFFFFFFF), если сумма переполняется.

Целью является x86 и ARM с использованием gcc (4.1.2) и Visual Studio (только для моделирования, поэтому здесь можно использовать резервную реализацию).

Это было полезно?

Решение

Вы, вероятно, хотите портативный C здесь код, который ваш компилятор превратит в правильную сборку ARM.В ARM есть условные перемещения, и они могут быть обусловлены переполнением.Затем алгоритм добавляется и условно устанавливает место назначения в значение без знака (-1), если обнаружено переполнение.

uint16_t add16(uint16_t a, uint16_t b)
{
  uint16_t c = a + b;
  if (c<a) /* Can only happen due to overflow */
    c = -1;
  return c;
}

Обратите внимание, что он отличается от других алгоритмов тем, что исправляет переполнение, а не полагается на другие вычисления для обнаружения переполнения.

x86-64 вывод clang 3.7 -O3 для adds32:значительно лучше, чем любой другой ответ:

    add     edi, esi
    mov     eax, -1
    cmovae  eax, edi
    ret

ARMv7: gcc 4.8 -O3 -mcpu=cortex-a15 -fverbose-asm вывод для adds32:

    adds    r0, r0, r1      @ c, a, b
    it      cs
    movcs   r0, #-1         @ conditional-move
    bx      lr

16бит:до сих пор не использует инструкцию добавления беззнакового насыщения ARM (UADD16)

    add     r1, r1, r0        @ tmp114, a
    movw    r3, #65535      @ tmp116,
    uxth    r1, r1  @ c, tmp114
    cmp     r0, r1    @ a, c
    ite     ls        @
    movls   r0, r1        @,, c
    movhi   r0, r3        @,, tmp116
    bx      lr  @

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

В простом C:

uint16_t sadd16(uint16_t a, uint16_t b)
    { return (a > 0xFFFF - b) ? 0xFFFF : a + b; }

uint32_t sadd32(uint32_t a, uint32_t b)
    { return (a > 0xFFFFFFFF - b) ? 0xFFFFFFFF : a + b;} 

который почти макрозирован и прямо передает смысл.

В IA32 без условных переходов:

uint32_t sadd32(uint32_t a, uint32_t b)
{
#if defined IA32
  __asm
  {
    mov eax,a
    xor edx,edx
    add eax,b
    setnc dl
    dec edx
    or eax,edx
  }
#elif defined ARM
  // ARM code
#else
  // non-IA32/ARM way, copy from above
#endif
}

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

В ARMv6 даже есть насыщенное сложение, вычитание и все остальное для 32-битных и упакованных чисел.

На x86 вы получаете насыщенную арифметику либо через MMX, либо через SSE.

Для всего этого нужен ассемблер, так что это не то, что вы просили.

Существуют также C-трюки для выполнения насыщенной арифметики.Этот небольшой код выполняет насыщенное сложение четырех байтов двойного слова.Он основан на идее параллельного расчета 32 полусумматоров, напримердобавление чисел без переполнения переноса.

Это делается в первую очередь.Затем переносы вычисляются, добавляются и заменяются маской, если сложение приведет к переполнению.

uint32_t SatAddUnsigned8(uint32_t x, uint32_t y) 
{
  uint32_t signmask = 0x80808080;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 7);
  return (x ^ t0) | t1;
}

Вы можете получить то же самое для 16 бит (или любого битового поля), изменив константу маски знака и сдвиги внизу следующим образом:

uint32_t SatAddUnsigned16(uint32_t x, uint32_t y) 
{
  uint32_t signmask = 0x80008000;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 15);
  return (x ^ t0) | t1;
}

uint32_t SatAddUnsigned32 (uint32_t x, uint32_t y)
{
  uint32_t signmask = 0x80000000;
  uint32_t t0 = (y ^ x) & signmask;
  uint32_t t1 = (y & x) & signmask;
  x &= ~signmask;
  y &= ~signmask;
  x += y;
  t1 |= t0 & x;
  t1 = (t1 << 1) - (t1 >> 31);
  return (x ^ t0) | t1;
}

Приведенный выше код делает то же самое для 16- и 32-битных значений.

Если вам не нужна функция, позволяющая функциям параллельно добавлять и насыщать несколько значений, просто замаскируйте нужные вам биты.В ARM вам также необходимо изменить константу маски знака, поскольку ARM не может загрузить все возможные 32-битные константы за один цикл.

Редактировать: Параллельные версии, скорее всего, медленнее, чем прямые методы, но они быстрее, если вам приходится насыщать более одного значения одновременно.

Если вас волнует производительность, вы Действительно хочу сделать подобные вещи в SIMD, где x86 имеет встроенную арифметику насыщения.

Из-за отсутствия насыщающей арифметики в скалярной математике можно получить случаи, когда операции, выполняемые над SIMD с 4 переменными, более более чем в 4 раза быстрее, чем эквивалентный C (и, соответственно, верно для SIMD с 8 переменными):

sub8x8_dct8_c: 1332 clocks
sub8x8_dct8_mmx: 182 clocks
sub8x8_dct8_sse2: 127 clocks

Решение с нулевым ответвлением:

uint32_t sadd32(uint32_t a, uint32_t b)
{
    uint64_t s = (uint64_t)a+b;
    return -(s>>32) | (uint32_t)s;
}

Хороший компилятор оптимизирует это, чтобы избежать выполнения каких-либо реальных 64-битных арифметических операций (s>>32 будет просто флагом переноса, и -(s>>32) является результатом sbb %eax,%eax).

В ассемблере x86 (синтаксис AT&T, a и b в eax и ebx, результат в eax):

add %eax,%ebx
sbb %eax,%eax
or %ebx,%eax

8- и 16-битные версии должны быть очевидны.Подписанная версия может потребовать немного больше работы.

uint32_t saturate_add32(uint32_t a, uint32_t b)
{
    uint32_t sum = a + b;
    if ((sum < a) || (sum < b))
        return ~((uint32_t)0);
    else
        return sum;
} /* saturate_add32 */

uint16_t saturate_add16(uint16_t a, uint16_t b)
{
    uint16_t sum = a + b;
    if ((sum < a) || (sum < b))
        return ~((uint16_t)0);
    else
        return sum;
} /* saturate_add16 */

Редактировать: Теперь, когда вы опубликовали свою версию, я не уверен, что моя чище/лучше/эффективнее/продуманнее.

Я не уверен, что это быстрее, чем решение Skizz (всегда профиль), но вот альтернативное решение для сборки без ветвей.Обратите внимание, что для этого требуется инструкция условного перемещения (CMOV), которая, я не уверен, доступна на вашей цели.


uint32_t sadd32(uint32_t a, uint32_t b)
{
    __asm
    {
        movl eax, a
        addl eax, b
        movl edx, 0xffffffff
        cmovc eax, edx
    }
}

Текущая реализация, которую мы используем:

#define sadd16(a, b)  (uint16_t)( ((uint32_t)(a)+(uint32_t)(b)) > 0xffff ? 0xffff : ((a)+(b)))
#define sadd32(a, b)  (uint32_t)( ((uint64_t)(a)+(uint64_t)(b)) > 0xffffffff ? 0xffffffff : ((a)+(b)))

Наилучшая производительность обычно достигается при встроенной сборке (как уже говорили некоторые).

Но для переносимого C эти функции включают только одно сравнение и не требуют приведения типов (и поэтому я считаю их оптимальными):

unsigned saturate_add_uint(unsigned x, unsigned y)
{
    if (y>UINT_MAX-x) return UINT_MAX;
    return x+y;
}

unsigned short saturate_add_ushort(unsigned short x, unsigned short y)
{
    if (y>USHRT_MAX-x) return USHRT_MAX;
    return x+y;
}

В качестве макросов они становятся:

SATURATE_ADD_UINT(x, y) (((y)>UINT_MAX-(x)) ? UINT_MAX : ((x)+(y)))
SATURATE_ADD_USHORT(x, y) (((y)>SHRT_MAX-(x)) ? USHRT_MAX : ((x)+(y)))

Я оставляю версии для «unsigned long» и «unsigned long long» в качестве упражнения для читателя.;-)

На всякий случай кто-то захочет узнать реализацию без ветвления с использованием 32-битных целых чисел с дополнением до двух.

Предупреждение!Этот код использует неопределенную операцию:«сдвинуть вправо на -1» и, следовательно, использует свойство Инструкция Intel Pentium SAL чтобы замаскировать операнд счетчика до 5 бит.

int32_t sadd(int32_t a, int32_t b){
    int32_t sum = a+b;
    int32_t overflow = ((a^sum)&(b^sum))>>31;
    return (overflow<<31)^(sum>>overflow);
 }

Это лучшая реализация, известная мне

Я полагаю, что лучший способ для x86 — использовать встроенный ассемблер для проверки флага переполнения после добавления.Что-то вроде:

add eax, ebx
jno @@1
or eax, 0FFFFFFFFh
@@1:
.......

Это не очень портативно, но, ИМХО, самый эффективный способ.

Альтернативой решению asm x86 без ветвей является (синтаксис AT&T, a и b в eax и ebx, результат в eax):

add %eax,%ebx
sbb $0,%ebx

Используя C++, вы можете написать более гибкий вариант Ремо.Дрешение:

template<typename T>
T sadd(T first, T second)
{
    static_assert(std::is_integral<T>::value, "sadd is not defined for non-integral types");
    return first > std::numeric_limits<T>::max() - second ? std::numeric_limits<T>::max() : first + second;
}

Это можно легко перевести на C, используя ограничения, определенные в limits.h.Также обратите внимание, что Целочисленные типы фиксированной ширины может быть недоступно в вашей системе.

//function-like macro to add signed vals, 
//then test for overlow and clamp to max if required
#define SATURATE_ADD(a,b,val)  ( {\
if( (a>=0) && (b>=0) )\
{\
    val = a + b;\
    if (val < 0) {val=0x7fffffff;}\
}\
else if( (a<=0) && (b<=0) )\
{\
    val = a + b;\
    if (val > 0) {val=-1*0x7fffffff;}\
}\
else\
{\
    val = a + b;\
}\
})

Я провел быстрый тест и, кажется, работает, но еще не сильно его критиковал!Это работает с SIGNED 32 бит.оп:редактор, используемый на веб-странице, не позволяет мне публиковать макросы, т. е. он не понимает синтаксис без отступов и т. д.!

int saturating_add(int x, int y)
{
    int w = sizeof(int) << 3;
    int msb = 1 << (w-1);

    int s = x + y;
    int sign_x = msb & x;
    int sign_y = msb & y;
    int sign_s = msb & s;

    int nflow = sign_x && sign_y && !sign_s;
    int pflow = !sign_x && !sign_y && sign_s;

    int nmask = (~!nflow + 1);
    int pmask = (~!pflow + 1);

    return (nmask & ((pmask & s) | (~pmask & ~msb))) | (~nmask & msb);
}

Эта реализация не использует потоки управления, операторы Campare(==, !=) и ?: оператор.Он просто использует побитовые операторы и логические операторы.

Арифметика насыщения не является стандартной для C, но она часто реализуется через встроенные функции компилятора, поэтому самый эффективный способ не будет самым чистым.Вы должны добавить #ifdef блоки, чтобы выбрать правильный путь.Ответ MSalters является самым быстрым для архитектуры x86.Для ARM вам нужно использовать __qadd16 функция (компилятор ARM) _arm_qadd16 (Microsoft Visual Studio) для 16-битной версии и __qadd для 32-битной версии.Они будут автоматически преобразованы в одну инструкцию ARM.

Ссылки:

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top