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

Представьте, что у меня есть два байта без знака 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);

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


person ovk    schedule 02.11.2015    source источник
comment
y ^ ((x ^ y) & -(x < y)) для int типов оценивает min(x, y) без ветвления. Это может стать частью возможного решения, основанного на том, что у вас есть.   -  person Bathsheba    schedule 02.11.2015
comment
y ^ ((x ^ y) & -(x < y)); это min(x,y) (обычно) без ветки (где x < y может, в зависимости от машины, все еще быть веткой), но в современной архитектуре есть условное перемещение, которое, вероятно, не намного медленнее.   -  person Pixelchemist    schedule 02.11.2015
comment
Возможно, полезно использовать Clamped Increment Increment?.   -  person Shafik Yaghmour    schedule 02.11.2015
comment
@ShafikYaghmour: я думаю, что это возможно. И если они использовали тип char, то, начиная с C++14, это должно быть дополнением до 2.   -  person Bathsheba    schedule 02.11.2015
comment
I just wonder if there are any better ways to do this, i.e. by some hacky bit manipulations? Если код не находится в очень критической части с точки зрения производительности, хакерские оптимизации в конечном итоге только усложняют чтение. Я предлагаю более конкретный вопрос, например ...are there any more efficient ways....   -  person eerorika    schedule 02.11.2015
comment
Это вопрос C или C++? Пожалуйста, выберите один.   -  person fuz    schedule 02.11.2015
comment
@Bathsheba действительно, я добавил ответ, так как код, созданный для случая uinit8_t на основе этого подхода, выглядит разумным.   -  person Shafik Yaghmour    schedule 02.11.2015
comment
Просто подтверждаю: вы бы предпочли неправильный ответ, чтобы остаться в пределах байта? 254 + 254 = 255, а 1 - 254 = 0?   -  person Alan Campbell    schedule 03.11.2015
comment
@AlanCampbell это называется Арифметика насыщения.   -  person Shafik Yaghmour    schedule 03.11.2015
comment
Вам нужно, чтобы он был портативным? Потому что если вы смотрите на конкретную архитектуру, то, вероятно, есть хорошая единственная инструкция. Я знаю, что ARM имеет насыщенное векторное сложение и вычитание для байтов. В X86 встроенная функция _mm_adds_epi8 выполняет насыщенное добавление 16 байтов в одной инструкции.   -  person porglezomp    schedule 03.11.2015
comment
@porglezomp, да, и _mm_subs_epi8 для вычитания.   -  person Z boson    schedule 03.11.2015
comment
как сказал @FUZxxl, можете ли вы пояснить, почему вы пометили как C, так и C++. На первый взгляд это больше похоже на вопрос C, и подавляющее большинство ответов ориентированы на C.   -  person Shafik Yaghmour    schedule 03.11.2015
comment
Очевидно, это означает, что я использую компилятор C++, поэтому можно использовать что-либо из стандартной библиотеки (например, std::min), но решения на чистом C также приемлемы.   -  person ovk    schedule 03.11.2015


Ответы (11)


В статье Арифметика насыщения без ветвей приведены стратегии для этого:

Решение их сложения выглядит следующим образом:

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;
}
person Shafik Yaghmour    schedule 02.11.2015
comment
Является ли это решение переносимым? Я думаю, предполагается, что -1 представлен в форме дополнения 2 (все биты установлены в 1). - person user1969104; 03.11.2015
comment
@user1969104 user1969104 это может быть так, но, как указано в комментарии к статье, это решается путем приведения к беззнаковому перед применением унарного минуса. На практике маловероятно, что вам придется иметь дело с чем-то еще, кроме двух дополнений. - person Shafik Yaghmour; 03.11.2015
comment
Я понял, что это может быть переносимым из-за использования неподписанных типов. -1 должно быть наибольшим беззнаковым значением, подобным арифметике переполнения. Однако я не уверен, является ли результат (res < x) беззнаковым или требует приведения типов. - person user1969104; 03.11.2015
comment
Это может быть хороший ответ C, но не очень хороший ответ C++. - person Yakk - Adam Nevraumont; 03.11.2015
comment
@Yakk Я хотел подумать об этом с точки зрения C ++, но у меня еще не было возможности. - person Shafik Yaghmour; 03.11.2015
comment
@Yakk Что делает это плохим ответом на С++? Это базовые математические операции, и я не понимаю, как это будет интерпретироваться только как C или как плохой C++. - person JPhi1618; 03.11.2015
comment
@ JPhi1618 JPhi1618 Лучшим ответом на С++ может быть template<class T>struct sat{T t;}; с перегруженными операторами, которые насыщают? Правильное использование пространств имен. В основном сахар. - person Yakk - Adam Nevraumont; 03.11.2015
comment
@Yakk, а, хорошо. Я просто видел это как минимальный пример, который ОП может адаптировать по мере необходимости. Я бы не ожидал увидеть такую ​​полную реализацию. Спасибо за разъяснения. - person JPhi1618; 03.11.2015

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

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 секунды. Это решение также более читабельно.

person user1969104    schedule 02.11.2015
comment
@user694733 user694733 Это не оптимизировано, оно оптимизировано для условного назначения в зависимости от флага переноса. - person fuz; 02.11.2015
comment
Да, user694733 прав. Он оптимизирован в условное присваивание. - person user1969104; 02.11.2015
comment
Это не будет работать для всех случаев, например, badd: b = 155 x = 201, чем badd = 156, а это больше, чем b. Вам нужно будет сравнить результат с min() или max() двух переменных, в зависимости от операции - person Cristian F; 17.11.2015
comment
@CristianF Как посчитать 155+201 = 156? Я думаю, что это должно быть 155 + 201 = 356% 256 = 100. Я не думаю, что min(), max() необходимы в любой комбинации значений b, x. - person user1969104; 17.11.2015

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

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);
person chux - Reinstate Monica    schedule 02.11.2015
comment
Для sum возможно (a + b) | -(a <= (255 - b))? - person R_Kapp; 02.11.2015
comment
Вы могли сделать sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF, предполагая sizeof(int) > sizeof(unsigned char), но это выглядит настолько сложным, что я не знаю, выиграете ли вы от этого что-нибудь (кроме головной боли). - person user694733; 02.11.2015
comment
@user694733 user694733 Да и, может быть, даже (a+b+1)*(a <= (255-b)) - 1. - person chux - Reinstate Monica; 02.11.2015
comment
@NathanOliver Спасибо за оплошность - показательным аспектом этого является то, что sub было легко, поскольку предел был 0. Но другие ограничения создают сложности и следуют комментарию user2079303. - person chux - Reinstate Monica; 02.11.2015
comment
Это решение генерирует гораздо больше ASM-кода, чем мое решение с флагом -O2 для gcc. - person user1969104; 02.11.2015
comment
@user1969104 user1969104 OP не был уверен, что лучше (пространство кода по сравнению со скоростью работы), ни целевая платформа, ни компилятор. Оценка скорости имеет смысл в контексте неопубликованной более крупной проблемы. - person chux - Reinstate Monica; 02.11.2015
comment
@chux Я понимаю. Просто из любопытства я проверил процессор Intel на 64-битной машине Ubuntu. Я измерил это и опубликовал результаты в своем решении. - person user1969104; 02.11.2015
comment
Мне кажется, что умножение на bools скрывает намерение; вероятно, для будущих пользователей было бы лучше быть более явными с условными выражениями. - person Kyle Kanos; 03.11.2015
comment
@Kyle Kanos Деталь: в C результаты операторов отношения, таких как >=, равны int, а не bool. лучше, к сожалению, в сообщении ОП неясно, что касается эффективности выполнения, размера кода или ясности исходного кода. Я предполагаю, что OP искал эффективность - что-то сильно зависящее от машины/компилятора - и поэтому предлагал код, который может работать быстрее. YMMV. - person chux - Reinstate Monica; 03.11.2015
comment
@chux: в настоящее время я использую C ++ больше, чем C, и OP использовал оба тега, поэтому комментарий int vs bool. Я использую слово «лучше» для понимания кода будущими пользователями, а не для неопределенных улучшений, требуемых OP. - person Kyle Kanos; 03.11.2015
comment
@ Кайл Канос, да, теперь я тоже помечен на двух языках. Это один из тех случаев, когда C и C++ немного расходятся. - person chux - Reinstate Monica; 03.11.2015

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

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}
person erebos    schedule 03.11.2015
comment
Это лучший ответ. Использование встроенных компиляторов вместо битовой магии не только быстрее, но и понятнее, а также упрощает сопровождение кода. - person Cephalopod; 03.11.2015
comment
Спасибо, @erebos. Я обязательно попробую это на платформах, где это доступно. - person ovk; 03.11.2015
comment
Я не могу заставить gcc генерировать безбрачный код с этим, что немного разочаровывает. Особенно прискорбно то, что clang использует для них разные имена. - person Shafik Yaghmour; 03.11.2015
comment
@Cephalopod И это совершенно не кроссплатформенно, черт возьми, скорее всего, даже не работает на другом компиляторе. Не лучшее решение для 21 века. - person Ela782; 04.11.2015
comment
@ Ela782 Ela782 Все как раз наоборот: встроенные модули - не лучшее решение для 20-го века. Добро пожаловать в будущее! - person Cephalopod; 04.11.2015
comment
@ShafikYaghmour Я сделал ответ, используя встроенные/внутренние функции без ветвления stackoverflow.com/a/33527635/1681678 - person MichaelMitchell; 05.11.2015
comment
@Cephalopod Я не понимаю, как использование нестандартных, специфичных для компилятора вещей может быть полезным. Если бы встроенные модули были стандартизированы, я бы с вами согласился, но я очень сомневаюсь, что это так. - person Ela782; 05.11.2015

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

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);

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

person supercat    schedule 02.11.2015

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

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

Мы можем использовать 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. Отсутствие carry_flag дает 0, поэтому !carry_flag * result = 0 при переполнении. А поскольку 0 - 1 установит максимальное целочисленное значение без знака, функция вернет результат сложения, если переноса нет, и вернет максимальное значение выбранного интегрального значения, если перенос есть.

person MichaelMitchell    schedule 04.11.2015
comment
Возможно, вы захотите упомянуть, что этот ответ предназначен для конкретной архитектуры набора инструкций (x86?) и потребует повторной реализации для каждой целевой архитектуры (SPARC, MIPS, ARM и т. д.). - person Toby Speight; 04.03.2019

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

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

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;
person Community    schedule 02.11.2015
comment
Я исправил (очевидную?) опечатку, но все еще не думаю, что это правильно. - person Bathsheba; 02.11.2015
comment
Это также включает ветвление. - person fuz; 02.11.2015
comment
Я удалю этот ответ, просто быстрый вопрос в сборке без оптимизации, в чем разница между тернарным оператором и оператором if/else? - person ; 02.11.2015
comment
@GRC Нет никакой разницы. - person fuz; 02.11.2015
comment
@GRC FUZxxl прав, но, как всегда, попробуйте сами. Даже если вы не знаете ассемблера (вы можете задать вопрос здесь, на SO, если вам что-то непонятно), просто проверив длину/инструкции, вы будете знать. - person edmz; 02.11.2015
comment
Ребята, я сделал это, есть разница и, в отличие от версии if/else, тернарная операция не включает ни одного оператора перехода. - person ; 03.11.2015

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

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

// Subtraction without underflow
return (b > a) ? 0 : a - b;
person Yves Daoust    schedule 02.11.2015
comment
На самом деле это одно из лучших решений. Все остальные, выполняющие вычитание или сложение до этого, на самом деле создают неопределенное поведение в C++, в результате чего компилятор может делать все, что захочет. На практике вы можете в основном предсказать, что произойдет, но все же. - person Adrien Hamelin; 07.11.2015

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

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

person gnasher729    schedule 03.11.2015

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

person Robert Ramey    schedule 02.11.2015
comment
Предоставление примера того, как использовать библиотеку, сделает этот ответ лучшим. Кроме того, они дают гарантию без брака? - person Shafik Yaghmour; 02.11.2015
comment
Библиотека имеет обширную документацию и примеры. Но, в конце концов, это так же просто, как включить соответствующий заголовок и заменить int на safe‹int›. - person Robert Ramey; 03.11.2015
comment
без ветвей? Я предполагаю, что вы человек без ветвей. Библиотека использует метапрограммирование шаблонов для включения проверок во время выполнения только при необходимости. Например, unsigned char, умноженный на unsigned char, приведет к unsigned int. Это никогда не может переполниться, поэтому вообще не нужно выполнять проверку. С другой стороны, unsigned times unsigned может переполниться, поэтому его необходимо проверять во время выполнения. - person Robert Ramey; 03.11.2015

Если вы будете часто вызывать эти методы, самым быстрым способом будет не битовая манипуляция, а, вероятно, справочная таблица. Определите массив длиной 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-битные вычисления выполняются немного быстрее. Вам решать

person DanielHsH    schedule 02.11.2015
comment
На самом деле, скорее всего, не будет: во-первых, конечно, таблица загружается медленно. Битовые операции занимают 1 такт, загрузка из памяти занимает примерно 80 нс; даже из кеша L1 мы находимся в диапазоне 20 нс, что составляет почти 7 циклов для процессора с тактовой частотой 3 ГГц. - person edmz; 02.11.2015
comment
Вы не совсем правы. Метод LUT займет несколько циклов, но манипуляция с битами также не является одним циклом. Есть несколько последовательных действий. Например, только для вычисления MAX() требуется 2 вычитания, логическая операция и один сдвиг вправо. И не забывайте о целочисленном повышении/понижении - person DanielHsH; 02.11.2015
comment
Я хотел сказать, что одиночные побитовые операции занимают 1 цикл, естественно, предполагая регистровые операнды. С кодом, который показал Шафик, clang выводит 4 элементарные инструкции. Также (x > y) не имеет ветвей. - person edmz; 02.11.2015
comment
Во-первых, (x › y) может использовать ветвление. Вы не знаете, на какой архитектуре работаете. Я склонен согласиться с тем, что на архитектуре Intel, возможно, нет ответвлений. Большинство смартфонов не Intel. Это также причина того, что вы не можете знать, сколько будет инструкций по сборке. Попробуйте мое решение на своем ПК. Мне интересно услышать результаты. - person DanielHsH; 02.11.2015
comment
Как это не может быть без ветвей? Ветвление принимается, когда вам нужно прыгать в зависимости от результата сравнения (как в if-else/?); в этом случае вы просто берете результат операции (т.е. затронутые флаги). - person edmz; 02.11.2015
comment
Кэш L1 намного быстрее, чем 20 нс, это примерно 4 такта процессора. И, вероятно, будет использовать неиспользуемый исполнительный блок, и в любом случае будет полностью конвейерным. Измерьте это. А 20 нс — это 60 циклов для процессора с частотой 3 ГГц. - person gnasher729; 03.11.2015
comment
На некоторых языках ассемблера (x›y) реализован как оператор c (x›y) ? 1 : 0; Имеет разветвления. Что касается времени обработки - мои измерения были неопределенны. Я протестировал LUT (таблицу поиска) по коду «Shafiks», и на некоторых аппаратных средствах LUT побеждает, на другом коде «Shafiks». Преимущество LUT в том, что его производительность меньше зависит от конкретных языков ассемблера и флагов оптимизации компилятора (а также гарантируется отсутствие ветвей на каждой архитектуре). - person DanielHsH; 03.11.2015
comment
@DanielHsH: Что вы думаете о моем подходе для процессоров, которые не могут выполнять x›y без ответвлений? Если исходные значения находятся в регистрах и в дальнейшем не нужны, и если результат не нужно маскировать, то, вероятно, будет около 4 инструкций: сложение, сдвиг и перемещение, отрицание или использование только двух исходных регистры. Если результат необходимо замаскировать, это, вероятно, добавит одну инструкцию. - person supercat; 03.11.2015