Вычитание / добавление насыщения для беззнаковых байтов


83

Представьте, что у меня есть два байта без знака bи x. Мне нужно рассчитать bsubкак b - xи baddкак b + x. Однако я не хочу, чтобы во время этих операций происходило переполнение / переполнение. Например (псевдокод):

b = 3; x = 5;
bsub = b - x; // bsub must be 0, not 254

и

b = 250; x = 10;
badd = b + x; // badd must be 255, not 4

Очевидный способ сделать это включает ветвление:

bsub = b - min(b, x);
badd = b + min(255 - b, x);

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


13
y ^ ((x ^ y) & -(x < y))для intтипов оценивается min(x, y)без ветвления. Это может стать частью окончательного решения, основанного на том, что у вас есть на данный момент.
Вирсавия


8
Это вопрос C или C ++? Пожалуйста, выберите один.
fuz 02


7
Вам нужно, чтобы он был портативным? Потому что, если вы смотрите на конкретную архитектуру, то, вероятно, есть одна хорошая инструкция. Я знаю, что в ARM есть добавление и вычитание векторов насыщения для байтов. На X86 _mm_adds_epi8встроенная функция выполняет насыщающее добавление 16 байтов за одну инструкцию.
porglezomp

Ответы:


86

В статье Branchfree Saturating Arithmetic представлены стратегии для этого:

Их дополнительный раствор выглядит следующим образом:

u32b sat_addu32b(u32b x, u32b y)
{
    u32b res = x + y;
    res |= -(res < x);

    return res;
}

изменено для uint8_t:

uint8_t  sat_addu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x + y;
    res |= -(res < x);

    return res;
}

и их решение вычитания:

u32b sat_subu32b(u32b x, u32b y)
{
    u32b res = x - y;
    res &= -(res <= x);

    return res;
}

изменено для uint8_t:

uint8_t sat_subu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x - y;
    res &= -(res <= x);

    return res;
}

2
@ user1969104, это может быть так, но, как указывает комментарий в статье, это решается преобразованием в unsigned перед применением унарного минуса. На практике маловероятно, что вам придется иметь дело с чем-то еще, кроме двух дополнений .
Шафик Ягмур

2
Это может быть хороший ответ C, но не очень хороший ответ C ++.
Yakk - Адам Неврамонт

4
@Yakk Что делает это "плохим" ответом C ++? Это базовые математические операции, и я не понимаю, как это будет интерпретировано только как C или как плохой C ++.
JPhi1618 03

4
@ JPhi1618 Лучший ответ C ++ может быть template<class T>struct sat{T t;};с перегруженными операторами, которые насыщают? Правильное использование пространств имен. В основном сахар.
Якк - Адам Неврамонт

6
@Yakk, А, ладно. Я просто видел это как минимальный пример, который OP может адаптировать по мере необходимости. Я не ожидал увидеть такую ​​полную реализацию. Спасибо за разъяснения.
JPhi1618 03

40

Простой метод - обнаружить переполнение и соответственно сбросить значение, как показано ниже.

bsub = b - x;
if (bsub > b)
{
    bsub = 0;
}

badd = b + x;
if (badd < b)
{
    badd = 255;
}

GCC может оптимизировать проверку переполнения в условное присвоение при компиляции с -O2.

Я замерил, насколько оптимизация по сравнению с другими решениями. При более чем 1000000000 операций на моем компьютере это решение и решение @ShafikYaghmour в среднем занимали 4,2 секунды, а решение @chux - 4,8 секунды. Это решение также более читабельно.


5
@ user694733 Он не оптимизирован, он оптимизирован для условного присвоения в зависимости от флага переноса.
fuz

2
Да user694733 прав. Он оптимизирован для условного присвоения.
user1969104 02

Это не будет работать для всех случаев, например badd: b = 155 x = 201, чем badd = 156, и это больше, чем b. Вам нужно будет сравнить результат с min () или max () двух переменных, в зависимости от операции,
Кристиан Ф

@CristianF Как вычислить 155 + 201 = 156? Я думаю, что это должно быть 155 + 201 = 356% 256 = 100. Я не думаю, что min (), max () нужны в любой комбинации значений b, x.
user1969104

16

Для вычитания:

diff = (a - b)*(a >= b);

Дополнение:

sum = (a + b) | -(a > (255 - b))

Эволюция

// sum = (a + b)*(a <= (255-b)); this fails
// sum = (a + b) | -(a <= (255 - b)) falis too

Спасибо @R_Kapp

Спасибо @NathanOliver

Это упражнение показывает ценность простого кодирования.

sum = b + min(255 - b, a);

Для sumвозможно (a + b) | -(a <= (255 - b))?
R_Kapp 02

Вы могли бы это сделать sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF, если предположить sizeof(int) > sizeof(unsigned char), но это выглядит настолько сложным, что я не знаю, выиграете ли вы от этого что-нибудь (кроме головной боли).
user694733 02

@ user694733 Да и может даже (a+b+1)*(a <= (255-b)) - 1.
chux

@NathanOliver Спасибо за контроль - показательный аспект в том, что это subбыло легко, как и предел 0. Но другие ограничения создают сложности и следуйте комментарию user2079303 .
chux

1
У @ user1969104 OP не было четкого определения «лучше» (размер кода в сравнении с быстродействием), ни целевой платформы, ни компилятора. Оценка скорости имеет наибольший смысл в контексте неопубликованной более крупной проблемы.
chux

13

Если вы используете достаточно свежую версию gcc или clang (возможно, также некоторые другие), вы можете использовать встроенные модули для обнаружения переполнения.

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}

Это лучший ответ. Использование встроенных модулей компилятора вместо битовой магии не только быстрее, но и понятнее, и делает код более удобным в сопровождении.
Cephalopod

Спасибо, @erebos. Я обязательно попробую это на платформах, где это доступно.
ovk 03

3
Я не могу заставить gcc генерировать безбрачный код с этим, что немного разочаровывает. Особенно прискорбно то, что clang использует для них разные имена .
Шафик Ягмур

1
@Cephalopod И это совершенно не кроссплатформенный, черт возьми, скорее всего, он даже не работает на другом компиляторе. Не лучшее решение для 21 века.
Ela782

1
@ Ela782 Как раз наоборот: встроенные модули - не лучшее решение для 20 века. Добро пожаловать в будущее!
Cephalopod

3

Для дополнения:

unsigned temp = a+b;  // temp>>8 will be 1 if overflow else 0
unsigned char c = temp | -(temp >> 8);

Для вычитания:

unsigned temp = a-b;  // temp>>8 will be 0xFF if neg-overflow else 0
unsigned char c = temp & ~(temp >> 8);

Операторы сравнения или умножения не требуются.


3

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

Для вычитания:

Можем воспользоваться sbbинструкцией

В MSVC мы можем использовать встроенную функцию _subborrow_u64 (также доступна в других битовых размерах).

Вот как это используется:

// *c = a - (b + borrow)
// borrow_flag is set to 1 if (a < (b + borrow))
borrow_flag = _subborrow_u64(borrow_flag, a, b, c);

Вот как мы можем применить это к вашей ситуации

uint64_t sub_no_underflow(uint64_t a, uint64_t b){
    uint64_t result;
    borrow_flag = _subborrow_u64(0, a, b, &result);
    return result * !borrow_flag;
}

Для дополнения:

Можем воспользоваться adcxинструкцией

В MSVC мы можем использовать встроенную функцию _addcarry_u64 (также доступна в других битовых размерах).

Вот как это используется:

// *c = a + b + carry
// carry_flag is set to 1 if there is a carry bit
carry_flag = _addcarry_u64(carry_flag, a, b, c);

Вот как мы можем применить это к вашей ситуации

uint64_t add_no_overflow(uint64_t a, uint64_t b){
    uint64_t result;
    carry_flag = _addcarry_u64(0, a, b, &result);
    return !carry_flag * result - carry_flag;
}

Мне этот метод не нравится так сильно, как метод вычитания, но я думаю, что он довольно изящный.

Если добавить переполняется, carry_flag = 1. Not-ing carry_flagдает 0, поэтому !carry_flag * result = 0при переполнении. А поскольку 0 - 1установит максимальное целое значение без знака, функция вернет результат сложения, если нет переноса, и вернет максимум выбранного интегрального значения, если есть перенос.


1
Возможно, вы захотите упомянуть, что этот ответ предназначен для конкретной архитектуры с набором инструкций (x86?) И потребует повторной реализации для каждой целевой архитектуры (SPARC, MIPS, ARM и т. Д.)
Тоби Спейт

2

что насчет этого:

bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;

Я исправил (очевидную?) Опечатку, но все еще не думаю, что это правильно.
Вирсавия

Это также включает ветвление.
fuz 02

Я удалю этот ответ, просто быстрый вопрос в сборке без оптимизации, в чем разница между тернарным оператором и оператором if / else?

@GRC Нет разницы.
fuz 02

@GRC FUZxxl прав, но, как всегда, попробуйте сами. Даже если вы не разбираетесь в сборке (вы можете задать вопрос здесь, если вам что-то непонятно), просто проверив длину / инструкции, которые вы знаете.
edmz 02

2

Все может быть выполнено в беззнаковой байтовой арифметике

// Addition without overflow
return (b > 255 - a) ? 255 : a + b

// Subtraction without underflow
return (b > a) ? 0 : a - b;

1
На самом деле это одно из лучших решений. Все остальные, выполняющие вычитание или сложение до этого, фактически создают неопределенное поведение в C ++, в результате чего компилятор может делать все, что хочет. На практике в основном можно предсказать, что произойдет, но все же.
Adrien Hamelin

2

Если вы хотите сделать это с двумя байтами, используйте самый простой из возможных код.

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


2

Вы также можете использовать безопасную цифровую библиотеку в Boost Library Incubator . Он обеспечивает замену для int, long и т. Д., Что гарантирует, что вы никогда не получите необнаруженного переполнения, потери значимости и т. Д.


7
Предоставление примера того, как использовать библиотеку, сделало бы это лучшим ответом. Кроме того, дают ли они безобидную гарантию?
Шафик Ягмур

В библиотеке есть обширная документация и примеры. Но в конце концов это так же просто, как включить соответствующий заголовок и заменить безопасный <int> на int.
Роберт Рэйми

безотказный? Я думаю, ты человек без филиалов. Библиотека использует метапрограммирование шаблонов для включения проверок времени выполнения только при необходимости. Например, unsigned char умножение на unsigned char приведет к unsigned int. Он никогда не может переполниться, поэтому никаких проверок проводить не нужно. С другой стороны, время без знака unsigned может переполняться, поэтому его необходимо проверять во время выполнения.
Роберт Рэми

1

Если вы будете часто вызывать эти методы, самым быстрым способом будет не битовая манипуляция, а, вероятно, таблица поиска. Определите массив длиной 511 для каждой операции. Пример на минус (вычитание)

static unsigned char   maxTable[511];
memset(maxTable, 0, 255);           // If smaller, emulates cutoff at zero
maxTable[255]=0;                    // If equal     - return zero
for (int i=0; i<256; i++)
    maxTable[255+i] = i;            // If greater   - return the difference

Массив статичен и инициализируется только один раз. Теперь ваше вычитание можно определить как встроенный метод или с помощью прекомпилятора:

#define MINUS(A,B)    maxTable[A-B+255];

Как это устроено? Ну, вы хотите предварительно вычислить все возможные вычитания для беззнаковых символов. Результаты варьируются от -255 до +255, всего 511 различных результатов. Мы определяем массив всех возможных результатов, но поскольку в C мы не можем получить к нему доступ по отрицательным индексам, мы используем +255 (в [A-B + 255]). Вы можете удалить это действие, указав указатель на центр массива.

const unsigned char *result = maxTable+255;
#define MINUS(A,B)    result[A-B];

используйте это как:

bsub  = MINUS(13,15); // i.e 13-15 with zero cutoff as requested

Обратите внимание, что выполнение происходит очень быстро. Только одно вычитание и одно указание, чтобы получить результат. Нет ветвления. Статические массивы очень короткие, поэтому они будут полностью загружены в кеш процессора для дальнейшего ускорения вычислений.

То же самое будет работать для сложения, но с немного другой таблицей (первые 256 элементов будут индексами, а последние 255 элементов будут равны 255, чтобы имитировать отсечение выше 255.

Если вы настаиваете на работе с битами, ответы, в которых используется (a> b), неверны. Это все еще может быть реализовано как ветвление. Используйте знаковый бит

// (num1>num2) ? 1 : 0
#define        is_int_biggerNotEqual( num1,num2) ((((__int32)((num2)-(num1)))&0x80000000)>>31)

Теперь вы можете использовать его для вычисления вычитания и сложения.

Если вы хотите эмулировать функции max (), min () без разветвления, используйте:

inline __int32 MIN_INT(__int32 x, __int32 y){   __int32 d=x-y; return y+(d&(d>>31)); }              

inline __int32 MAX_INT(__int32 x, __int32 y){   __int32 d=x-y; return x-(d&(d>>31)); }

В моих примерах выше используются 32-битные целые числа. Вы можете изменить его на 64, хотя я считаю, что 32-битные вычисления выполняются немного быстрее. Вам решать


2
На самом деле, скорее всего, не будет: сначала, конечно, загрузка таблицы происходит медленно. Битовые операции занимают 1 цикл, загрузка из памяти - примерно 80 нс; даже из кеша L1 мы находимся в диапазоне 20 нс, что составляет почти 7 циклов на процессоре с тактовой частотой 3 ГГц.
edmz 02

Вы не совсем правы. Метод LUT займет несколько циклов, но манипуляции с битами также не являются одним циклом. Есть несколько последовательных действий. Например, только для вычисления MAX () требуется 2 вычитания, логическая операция и один сдвиг вправо. И не забывайте целочисленное повышение / понижение в должности
DanielHsH

1
Я хотел сказать, что одиночные побитовые операции занимают 1 цикл, естественно предполагая регистровые операнды. Используя код, который показал Шафик, clang выводит 4 элементарных инструкции. Также (x > y)вне филиала.
edmz 02

Во-первых, (x> y) может использовать ветвление. Вы не знаете, на какой архитектуре вы работаете. Я склонен согласиться с тем, что на архитектуре Intel он, возможно, не имеет ветвей. Большинство смартфонов не Intel. Это также причина того, что вы не можете знать, сколько будет инструкций по сборке. Попробуйте мое решение на своем ПК. Мне интересно услышать результаты.
DanielHsH

1
Кэш L1 намного быстрее, чем 20 нс, это порядка 4 циклов процессора. И, вероятно, будет использовать неиспользуемую в противном случае исполнительную единицу и в любом случае будет полностью конвейерной. Измерьте это. А 20ns - это 60 циклов в процессоре с тактовой частотой 3 ГГц.
gnasher729 03
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.