Насичуючи віднімання / додавання для непідписаних байтів


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

9
@AlanCampbell це називається насичувальною арифметикою .
Шафік Ягмор

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, що може бути так, але, як зазначає коментар у статті, це вирішується шляхом приведення до беззнаку перед застосуванням одинарного мінуса. На практиці навряд чи вам доведеться мати справу з чимось іншим, крім доповнення двох .
Шафік Ягмор

2
Це може бути гарною відповіддю на С, але це не дуже гарна відповідь на С ++.
Якк - Адам Невраумонт

4
@Yakk Що робить цю "погану" відповідь на C ++? Це основні математичні операції, і я не бачу, як би це трактувалось як лише на С або поганий С ++.
JPhi1618

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

6
@Yakk, Ах, добре. Я просто розглядав це як мінімальний приклад, який ОП може адаптувати за потреби. Я не очікував би бачити це повне впровадження. Дякую за роз'яснення.
JPhi1618

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
Так, користувач694733 має рацію. Він оптимізований під умовне призначення.
user1969104

Це не спрацювало б у всіх випадках, наприклад 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

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

@ 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;
}

Це найкраща відповідь. Використання вбудованих компіляторів замість бітової магії не тільки швидше, але й зрозуміліше і робить код більш ремонтопридатним.
Головоногий

Дякую, @erebos. Я обов’язково спробую це на платформах, де це доступно.
овк

3
Я не можу отримати gcc для генерації безіменного коду з цим, що трохи розчаровує. Особливо прикро тут те, що clang використовує різні назви для них .
Шафік Ягмор

1
@Cephalopod І це абсолютно не крос-платформа, чорт, швидше за все, навіть не працює на іншому компіляторі. Невдале рішення для 21 століття.
Ela782,

1
@ Ela782 Якраз навпаки: вбудовані системи не є хорошим рішенням для 20 століття. Ласкаво просимо в майбутнє!
Головоногий

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

Я видалю цю відповідь лише на коротке запитання в збірці без оптимізації, в чому різниця між трійковим оператором і оператором if / else?

@GRC Різниці немає.
fuz

@GRC FUZxxl має рацію, але, як завжди, спробуй сам. Навіть якщо ви не знаєте складання (ви можете задати питання тут на SO, якщо щось вам незрозуміло), просто перевіривши довжину / інструкції, які ви будете знати.
edmz

2

Все може бути зроблено в беззнаковій байтовій арифметиці

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

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

1
Це насправді одне з найкращих рішень. Всі інші, що робили раніше вилучення або додавання, насправді створюють невизначену поведінку в C ++, в результаті чого компілятор може робити все, що хоче. На практиці ви в основному можете передбачити, що буде, але все ж.
Адріен Хамелін,

2

Якщо ви хочете зробити це з двома байтами, використовуйте найпростіший можливий код.

Якщо ви хочете зробити це за допомогою двадцяти мільярдів байт, перевірте, які векторні інструкції є на вашому процесорі та чи можна їх використовувати. Ви можете виявити, що ваш процесор може виконати 32 з цих операцій за одну інструкцію.


2

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


7
Надання прикладу того, як користуватися бібліотекою, зробить це кращою відповіддю. Крім того, чи надають вони безгарантійну гарантію?
Шафік Ягмор

Бібліотека має велику документацію та приклади. Але в кінці дня це так само просто, як включити відповідний заголовок і замінити safe <int> на int.
Роберт Рамі,

без гілок? Я здогадуюсь ти людина без галузей. Бібліотека використовує метапрограмування шаблону, щоб включати перевірку часу виконання лише тоді, коли це необхідно. Наприклад, unsigned char times unsigned char призведе до unsigned int. Це ніколи не може переповнюватись, тому перевірки взагалі не потрібно робити. З іншого боку, непідписані рази без підпису можуть переповнюватися, тому його потрібно перевіряти під час виконання.
Роберт Рамі,

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,

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

1
Я мав на увазі сказати, що одиничні побітові операції займають 1 цикл, природно припускаючи операнди регістрів. З кодом, який показав Шафік, clang видає 4 елементарні інструкції. Крім того (x > y), без гілок.
edmz

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

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