Nasycenie odejmowania / dodawania dla bajtów bez znaku

83

Wyobraź sobie, że mam dwa bajty bez znaku bi x. Muszę obliczyć bsubjako b - xi baddjako b + x. Jednak nie chcę, aby podczas tych operacji wystąpił niedomiar / przepełnienie. Na przykład (pseudokod):

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

i

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

Oczywistym sposobem na to jest rozgałęzienie:

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

Zastanawiam się tylko, czy są jakieś lepsze sposoby, aby to zrobić, np. Przez jakieś hackerskie manipulacje?

ovk
źródło
13
y ^ ((x ^ y) & -(x < y))dla inttypów oblicza min(x, y)bez rozgałęzień. Może to stanowić część ostatecznego rozwiązania w oparciu o to, co masz do tej pory.
Batszeba
3
Być może zaciśnięta przyrostowa liczba całkowita? jest pomocny.
Shafik Yaghmour
8
Czy to jest pytanie w C czy C ++? Prosze wybrac jeden.
fuz
9
@AlanCampbell nazywa się Saturating Arithmetic .
Shafik Yaghmour
7
Czy potrzebujesz go, aby był przenośny? Ponieważ jeśli patrzysz na konkretną architekturę, prawdopodobnie jest to ładna pojedyncza instrukcja. Wiem, że ARM ma nasycające dodawanie i odejmowanie wektorów dla bajtów. Na X86 _mm_adds_epi8intrinsic dokona nasycenia 16 bajtów w pojedynczej instrukcji.
porglezomp

Odpowiedzi:

86

Artykuł Branchfree Saturating Arithmetic przedstawia strategie w tym zakresie:

Ich rozwiązanie do dodawania jest następujące:

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

    return res;
}

zmodyfikowany dla uint8_t:

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

    return res;
}

a ich rozwiązanie odejmowania to:

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

    return res;
}

zmodyfikowany dla uint8_t:

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

    return res;
}
Shafik Yaghmour
źródło
2
@ user1969104 to może być przypadek, ale jak wskazuje komentarz w artykule, jest to rozwiązywane przez rzutowanie na unsigned przed zastosowaniem jednoargumentowego minus. W praktyce jest mało prawdopodobne, abyś miał do czynienia z czymkolwiek innym niż uzupełnieniem dwóch .
Shafik Yaghmour
2
To może być dobra odpowiedź w C, ale nie jest to bardzo dobra odpowiedź w C ++.
Yakk - Adam Nevraumont
4
@Yakk Co sprawia, że ​​jest to „zła” odpowiedź C ++? To są podstawowe operacje matematyczne i nie widzę, jak byłoby to zinterpretowane jako tylko C lub zły C ++.
JPhi1618
4
@ JPhi1618 Lepszą odpowiedzią C ++ mogą być template<class T>struct sat{T t;};przeciążone operatory, które się nasycają? Właściwe użycie przestrzeni nazw. Głównie cukier.
Yakk - Adam Nevraumont
6
@Yakk, Ah, ok. Widziałem to jako minimalny przykład, który OP może dostosować w razie potrzeby. Nie spodziewałbym się, że implementacja będzie kompletna. Dzięki za wytłumaczenie.
JPhi1618
40

Prostą metodą jest wykrycie przepełnienia i odpowiednie zresetowanie wartości, jak poniżej

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

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

GCC może zoptymalizować sprawdzanie przepełnienia do przypisania warunkowego podczas kompilacji z -O2.

Zmierzyłem, ile optymalizacji w porównaniu z innymi rozwiązaniami. Przy 1000000000+ operacjach na moim komputerze rozwiązanie to i @ShafikYaghmour wynosiło średnio 4,2 sekundy, a @chux średnio 4,8 sekundy. To rozwiązanie jest również bardziej czytelne.

user1969104
źródło
5
@ user694733 Nie jest zoptymalizowany, jest zoptymalizowany do przypisania warunkowego w zależności od flagi przeniesienia.
piątek
2
Tak, użytkownik694733 ma rację. Jest zoptymalizowany do przypisania warunkowego.
user1969104
To nie zadziała we wszystkich przypadkach, na przykład badd: b = 155 x = 201, niż badd = 156, a to jest większe niż b. Będziesz musiał porównać wynik z min () lub max () dwóch zmiennych, w zależności od operacji
Cristian F
@CristianF Jak obliczyć 155 + 201 = 156? Myślę, że musi to być 155 + 201 = 356% 256 = 100. Nie sądzę, aby min (), max () było potrzebne w dowolnej kombinacji wartości b, x.
user1969104
16

Do odejmowania:

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

Dodanie:

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

Ewolucja

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

Dzięki @R_Kapp

Dzięki @NathanOliver

To ćwiczenie pokazuje wartość prostego kodowania.

sum = b + min(255 - b, a);
chux - Przywróć Monikę
źródło
Dla sumbyć może (a + b) | -(a <= (255 - b))?
R_Kapp
Mógłbyś to zrobić sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF, zakładając sizeof(int) > sizeof(unsigned char), ale wygląda to na tak skomplikowane, że nie wiem, czy coś z tego zyska (poza bólem głowy).
user694733
@ user694733 Tak, a może nawet (a+b+1)*(a <= (255-b)) - 1.
chux - Przywróć Monikę
@NathanOliver Dzięki za przeoczenie - wymownym aspektem jest to, że mimo sublimitu było to łatwe 0. Ale inne limity stwarzać komplikacje i postępuj user2079303 komentarz.
chux - Przywróć Monikę
1
@ user1969104 OP nie był jasny co do „lepszego” (przestrzeń kodu w porównaniu do szybkości działania), platformy docelowej ani kompilatora. Ocena szybkości ma największy sens w kontekście niepublikowanego większego problemu.
chux - Przywróć Monikę
13

Jeśli używasz wystarczająco aktualnej wersji gcc lub clang (może także innych), możesz użyć wbudowanych funkcji do wykrywania przepełnienia.

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}
erebos
źródło
To najlepsza odpowiedź. Używanie wbudowanych kompilatorów zamiast magii bitów jest nie tylko szybsze, ale także bardziej przejrzyste i sprawia, że ​​kod jest łatwiejszy w utrzymaniu.
Głowonóg
Dziękuję @erebos. Zdecydowanie spróbuję tego na platformach, na których jest to dostępne.
ovk
3
Nie mogę zmusić gcc do generowania bezramkowego kodu za pomocą tego kodu, co jest trochę rozczarowujące. Szczególnie niefortunne jest to, że Clang używa dla nich różnych nazw .
Shafik Yaghmour
1
@ Głowonóg I to zupełnie nie jest wieloplatformowe, do cholery, najprawdopodobniej nie działa nawet na innym kompilatorze. Nie jest to dobre rozwiązanie na XXI wiek.
Ela782
1
@ Ela782 Jest dokładnie odwrotnie: zabudowy nie są dobrym rozwiązaniem na XX wiek. Witamy w przyszłości!
Głowonóg
3

Dodatkowo:

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

Do odejmowania:

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

Nie są wymagane żadne operatory porównania ani mnożenia.

supercat
źródło
3

Jeśli chcesz skorzystać z montażu lub intrinsics, myślę, że mam optymalne rozwiązanie.

Do odejmowania:

Możemy skorzystać z sbbinstrukcji

W MSVC możemy użyć funkcji wewnętrznej _subborrow_u64 (dostępnej również w innych rozmiarach bitowych).

Oto, jak jest używany:

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

Oto, jak możemy to zastosować w twojej sytuacji

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

Dodatkowo:

Możemy skorzystać z adcxinstrukcji

W MSVC możemy użyć funkcji wewnętrznej _addcarry_u64 (dostępnej również w innych rozmiarach bitowych).

Oto, jak jest używany:

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

Oto, jak możemy to zastosować w twojej sytuacji

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

Nie lubię tego tak bardzo, jak odejmowania, ale myślę, że jest całkiem fajny.

Jeśli dodatek przelewa, carry_flag = 1. Noting carry_flagdaje 0, więc !carry_flag * result = 0gdy występuje przepełnienie. A ponieważ 0 - 1ustawi wartość całki bez znaku na maksimum, funkcja zwróci wynik dodawania, jeśli nie ma przeniesienia, i zwróci maksimum wybranej wartości całkowitej, jeśli istnieje przeniesienie.

Michael Mitchell
źródło
1
Możesz wspomnieć, że ta odpowiedź dotyczy konkretnej architektury zestawu instrukcji (x86?) I będzie wymagać ponownego wdrożenia dla każdej architektury docelowej (SPARC, MIPS, ARM itp.)
Toby Speight
2

a co z tym:

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

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

źródło
Poprawiłem (oczywistą?) Literówkę, ale nadal uważam, że to nie jest poprawne.
Batszeba
Obejmuje to również rozgałęzianie.
fuz
Usunę tę odpowiedź tylko szybkie pytanie w montażu bez optymalizacji. Jaka jest różnica między operatorem trójskładnikowym a instrukcją if / else?
@GRC Nie ma różnicy.
fuz
@GRC FUZxxl ma rację, ale jak zwykle spróbuj sam. Nawet jeśli nie znasz montażu (możesz zadać tutaj pytanie na SO, jeśli coś nie jest dla ciebie jasne), po prostu sprawdzając długość / instrukcje, które znasz.
edmz
2

Wszystko można wykonać w arytmetyce bajtów bez znaku

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

// Subtraction without underflow
return (b > a) ? 0 : a - b;
Yves Daoust
źródło
1
To właściwie jedno z najlepszych rozwiązań. Wszyscy inni dokonujący wcześniej odejmowania lub dodawania w rzeczywistości tworzą niezdefiniowane zachowanie w C ++, w wyniku czego kompilator może robić, co chce. W praktyce można głównie przewidzieć, co się stanie, ale jednak.
Adrien Hamelin
2

Jeśli chcesz to zrobić z dwoma bajtami, użyj najprostszego możliwego kodu.

Jeśli chcesz to zrobić z dwudziestoma miliardami bajtów, sprawdź, jakie instrukcje wektorowe są dostępne w twoim procesorze i czy można ich użyć. Może się okazać, że twój procesor może wykonać 32 z tych operacji za pomocą jednej instrukcji.

gnasher729
źródło
2

Możesz także skorzystać z bezpiecznej biblioteki numerycznej w Boost Library Incubator . Zapewnia zamienniki typu drop-in dla int, long itp., Które gwarantują, że nigdy nie dostaniesz niewykrytego przepełnienia, niedomiaru itp.

Robert Ramey
źródło
7
Podanie przykładu, jak korzystać z biblioteki, uczyniłoby to lepszą odpowiedzią. Ponadto, czy zapewniają bezramkową gwarancję?
Shafik Yaghmour
Biblioteka posiada obszerną dokumentację i przykłady. Ale na koniec dnia jest to tak proste, jak dodanie odpowiedniego nagłówka i zastąpienie safe <int> int.
Robert Ramey
bez gałęzi? Myślę, że jesteś człowiekiem bez gałęzi. Biblioteka korzysta z metaprogramowania szablonów w celu uwzględnienia kontroli czasu wykonywania tylko wtedy, gdy jest to konieczne. Na przykład unsigned char razy unsigned char da w wyniku unsigned int. To nigdy nie może się przepełnić, więc nie trzeba w ogóle sprawdzać. Z drugiej strony czasy bez znaku bez znaku mogą się przepełniać, więc należy je sprawdzić w czasie wykonywania.
Robert Ramey
1

Jeśli będziesz często wywoływał te metody, najszybszym sposobem nie byłaby manipulacja bitami, ale prawdopodobnie tabela przeglądowa. Zdefiniuj tablicę o długości 511 dla każdej operacji. Przykład na minus (odejmowanie)

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

Tablica jest statyczna i inicjowana tylko raz. Teraz odejmowanie można zdefiniować jako metodę inline lub za pomocą prekompilatora:

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

Jak to działa? Cóż, chcesz wstępnie obliczyć wszystkie możliwe odejmowania dla znaków bez znaku. Wyniki wahają się od -255 do +255, łącznie 511 różnych wyników. Definiujemy tablicę wszystkich możliwych wyników, ale ponieważ w C nie mamy do niej dostępu z ujemnych indeksów, używamy +255 (w [A-B + 255]). Możesz usunąć tę akcję, definiując wskaźnik do środka tablicy.

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

użyj go jak:

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

Zwróć uwagę, że wykonanie jest niezwykle szybkie. Tylko jedno odejmowanie i jedno uwzględnienie wskaźnika, aby otrzymać wynik. Bez rozgałęzień. Tablice statyczne są bardzo krótkie, więc zostaną w pełni załadowane do pamięci podręcznej procesora, aby jeszcze bardziej przyspieszyć obliczenia

To samo zadziała w przypadku dodawania, ale z nieco inną tabelą (pierwsze 256 elementów będzie indeksami, a ostatnie 255 elementów będzie równe 255, aby emulować odcięcie powyżej 255.

Jeśli nalegasz na operację na bitach, odpowiedzi, które używają (a> b) są błędne. To nadal może być realizowane jako rozgałęzienie. Użyj techniki znaków bitowych

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

Teraz możesz go użyć do obliczenia odejmowania i dodawania.

Jeśli chcesz emulować funkcje max (), min () bez rozgałęziania użyj:

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

W powyższych przykładach używam 32-bitowych liczb całkowitych. Możesz zmienić to na 64, chociaż uważam, że 32-bitowe obliczenia działają nieco szybciej. Zależy od Ciebie

DanielHsH
źródło
2
W rzeczywistości prawdopodobnie nie będzie: po pierwsze, oczywiście ładowanie tabeli jest powolne. Operacje bitowe trwają 1 cykl, ładowanie z pamięci trwa około 80 ns; nawet z pamięci podręcznej L1 jesteśmy w zakresie 20 ns, czyli prawie 7 cykli na procesorze 3GHz.
edmz
Nie masz całkowitej racji. Metoda LUT zajmie kilka cykli, ale manipulacja bitami również nie jest pojedynczym cyklem. Istnieje kilka sekwencyjnych działań. Na przykład, obliczenie tylko MAX () wymaga 2 odejmowań, operacji logicznej i jednego przesunięcia w prawo. I nie zapomnij o awansie / degradacji w
liczbach
1
Chciałem powiedzieć, że pojedyncze operacje bitowe trwają 1 cykl, naturalnie przyjmując operandy rejestru. Z kodem, który pokazał Shafik, clang wyświetla 4 podstawowe instrukcje. Jest również bez (x > y)gałęzi.
edmz
Po pierwsze, (x> y) może używać rozgałęzień. Nie wiesz, na jakiej architekturze pracujesz. Zgadzam się, że prawdopodobnie jest on bez gałęzi w architekturze Intela. Większość smartfonów nie jest Intel. Jest to również powód, dla którego nie możesz wiedzieć, ile będzie instrukcji montażu. Wypróbuj moje rozwiązanie na swoim komputerze. Jestem zainteresowany wynikami.
DanielHsH
1
Pamięć podręczna L1 jest znacznie szybsza niż 20 ns, jest rzędu może 4 cykli procesora. I prawdopodobnie będzie używał nieużywanej w inny sposób jednostki wykonawczej, a mimo to będzie w pełni potokowy. Zmierz to. A 20ns to 60 cykli w procesorze 3 GHz.
gnasher729