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?


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


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

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.


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

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

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.


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.


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;

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;

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.


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.


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


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.