Branchless clamp to [0..255] [pl]

Parę dni temu na blogu Gynvaela Coldwinda pojawił się wpis o bardzo fajnej tematyce – zrobienia clampa dowolnej liczby całkowitej do zakresu [0..255] bez skoków. Postanowiłem się przyjrzeć temu z bliska, bo problem jest dość ciekawy i nie pozwolę, żeby mi ktoś chleb zabierał ;P.

Napisałem więc dwie funkcje, które także dają poprawny wynik i są branchless:

int my1(int x)
{
  x -= (x - 255) & -(x > 255);
  x += (-x) & -(x < 0);
  return x;
}

Pierwsza instrukcja spowoduje ustawienie x na 255 tylko wtedy, gdy x > 255. Jakim trafem to działa? x > 255 wynosi 1 (w rzeczywistości to wynosi true, ale bool ma niejawną konwersję do int), jeśli, jak łatwo się domyślić, x jest większe od 255 i 0 w przeciwnym wypadku. Jeśli zafundujemy temu negację arytmetyczną, to dostaniemy (x > 255) ? (-1) : (0). Ponieważ (prawie?) wszystkie architektury używają uzupełnienia do dwóch, -1 jest równe 0xFF...FF, czyli ma wszystkie bity równe 1. Pamiętając, że cokolwiek & 0xFF...FF = cokolwiek oraz cokolwiek & 0 = 0 uzyskujemy x -= (x > 255) ? (x - 255) : (0). Pozostaje nam to tajemnicze x - 255. Ile wynosi cokolwiek - (cokolwiek - 255)? Po opuszczeniu nawiasów otrzymujemy cokolwiek - cokolwiek + 255. cokolwiek - cokolwiek wynosi zawsze 0, więc zostaje 255. Ostatecznie daje nam to x = (x > 255) ? (255) : (x).

Druga linia jest podobna – jest to równoważne x += (x < 0) ? (-x) : (0). x + 0 to x, co jest oczywiste, więc pomyślmy nad tym -x. x + (-x) to x - x, czyli ładne 0. Całość sprowadza się więc do x = (x < 0) ? (0) : (x).

Druga funkcja natomiast jest nieco inna (wydaje się dłuższa, ale w rzeczywistości po skompilowaniu jest mniejsza):

int my2(int r)
{
  int min = r ^ ((255 ^ r) & -(r > 255));
  int max = min ^ ((0 ^ min) & -(min < 0));
  return max;
}

W tym przypadku całość jest chyba odrobinę (ale tylko odrobinę, bo ciągle jest mnóstwo operatorów bitowych, więc wskaźnik WTF/min jest wciąż wysoki ;>) czytelniejsza, ze względu na porządne nazwy zmiennych. Pierwsza instrukcja oblicza min(255, r). Druga natomiast max(0, min). Zwracany jest max, czyli efektywnie max(0, min(255, r)). Nie będę teraz tłumaczył czemu to tak wygląda, bo przypadkowo mam w planach pisanie o tym w części drugiej minitutka pisania branchless code /EDIT (16th November 2009): plany oczywiście się nie sprawdziły ;>. Teraz planuję o napisać o tym w trzeciej części, trudno, musisz zaczekać ;>/ ;).

Teraz słowo o wydajności. Przetestowałem cztery funkcje – te dwie pokazane wyżej, funkcję Gynvaela oraz pewną niespodziankę. Co się okazało? Wyniki były dla mnie niespodzianką, bo wyszło na to, że jestem lamką i nie potrafię napisać porządnego branchless code – moje funkcje uzyskiwały zwykle nieco gorsze czasy niż wynalazek Gyna. Najbardziej jednak zdziwiła mnie zwycięska funkcja, która w prawie wszystkich przypadkach była szybsza niż pozostałe… że co? Mam podać jej kod? Ok, proszę bardzo:

int clamp(int r)
{
  if(255 < r)
    return 255;
  else if(r < 0)
    return 0;
  else
    return r;
}

Tak, to jest ta funkcja. Najprostsza możliwa implementacja tego clampa okazała się najszybsza. Nawet już nie wspominam o czytelności. Ech, nie ma to jak niespodzianki sprawiane przez kompilator ;).

…co? Że niby to nie jest branchless? W takim razie sprawdźmy sobie jej kod wynikowy:

__Z5clampi:
; arg = esp + 4
mov eax, [esp + 4] ; eax = arg
xor edx, edx       ; edx = 0
test eax, eax      ;
cmovns edx, eax    ; if(eax >= 0) edx = eax
mov eax, 0FFh      ; eax = 0xFF
cmp edx, 0FFh      ;
cmovle eax, edx    ; if(edx <= 0xFF) eax = edx
ret                ; return eax

Nie ma skoków, mamy tylko conditional moves, więc jest branchless. Amen.

~ - autor: Fanael w dniu Listopad 10, 2009.

Odpowiedzi: 5 to “Branchless clamp to [0..255] [pl]”

  1. If the input is always in the range [0x00,0xFF] then you can use this version:

    typedef unsigned char u8;
    inline u8 IncClamp8( u8 x )
    {
    	// c=0 -> x+1 = x + ~c
    	// c=1 -> x+0 = x + ~c
    	u8 n = ~x & 1;
    	u8 t = x + n;
    	return t;
    }
    /*
     00005:8a c8		 mov	 cl, al
     00007:f6 d1		 not	 cl
     00009:80 e1 01	 and	 cl, 1
     0000c:02 c1		 add	 al, cl
    */
  2. Sorry, wrong copy/paste :-)

    inline u8 IncClamp8( u8 x )
    {
    /*
    	00008:0f b7 c0	 movzx	 eax, ax
    	0000e:c1 f9 08	 sar	 ecx, 8
    	00011:f6 d1		 not	 cl
    	00013:80 e1 01	 and	 cl, 1
    	00016:02 c8		 add	 cl, al
    	// c=0 -> x+1 = x + ~c
    	// c=1 -> x+0 = x + ~c
    */
    	short c = ((x + 1) >> 8) & 1;
    	short t = (x + (c^1)) & 0xFF;
    	return (u8) t;
    }
  3. Hehe, neat, even though it’s “increment-and-clamp” and I was writing about just “clamp” ;) Perhaps I’ll even write a note about operations combined with clamping, could I mention your code in it? ;)

  4. Sure, the code & comments is public domain. Would love to see a faster x86 version.

    Corresponding branchless decrement-and-clamp. :)

    // return (x > 0) ? x-1 : 0;
    inline u8 DecClamp8( u8 x )
    {
    // c=0 -> x-1 = x + ((~c) & 1)
    // c=1 -> x-0 = x + ((~c) & 1)
    u16 c = ~((x - 1) >> 8) & 1;
    u8 t = x - c;
    return t;
    }

    Cheers

  5. You’ll want to look at: “Adding with saturation”

    http://bob.allegronetwork.com/prog/tricks.html#addsat

    // — 8 255)
    z = 255;

    This is an add-with-saturation; we add two numbers, then cap the result at a certain value. This code is going to be really slow in inner loops, simply because of the branching involved. Well, we’ll show you how to achieve this without branching at all! However, we’ll only be able to cap on values that are powers-of-2 minus one.

    First thing we do is add the numbers; We then look at the overflow bit, and base all our computations on that. If there is no overflow, then we mustn’t do any operation.

    z = x + y;
    int overflow = z & 256;
    int mask = overflow - (overflow >> 7);
    z ^= overflow;
    z |= mask;

    This works only if we have a free bit for the overflow detection. If not, then we’ll need to be more creative.

    Let’s assume that we have 4 bytes packed into one unsigned int. We’d like to add those of those packed byte vectors together, with saturation at 255 for each value independently.

    First, we’ll need to do the addition on all bits except for the most significant ones. Then, we’ll manually compute the carries, and determine overflow ourselves.

Dodaj komentarz

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Zmień )

Twitter picture

You are commenting using your Twitter account. Log Out / Zmień )

Facebook photo

You are commenting using your Facebook account. Log Out / Zmień )

Connecting to %s

 
Follow

Otrzymuj każdy nowy wpis na swoją skrzynkę e-mail.