Jak najszybsze znalezienie dwóch największych z pięciu małych liczb całkowitych

9

Używam odmiany 5-krzyżowego filtra środkowego na danych obrazu w małym systemie osadzonym, tj

    x
  x x x
    x

Algorytm jest naprawdę prosty: odczytaj 5 liczb całkowitych bez znaku, uzyskaj najwyższe 2, wykonaj kilka obliczeń i zapisz wynik na liczbach całkowitych bez znaku.

Ładne jest to, że 5 wartości całkowitych wejściowych mieści się w zakresie 0-20. Obliczona wartość całkowita również mieści się w zakresie 0-20!

Dzięki profilowaniu zorientowałem się, że uzyskanie dwóch największych liczb jest wąskim gardłem, więc chcę przyspieszyć tę część. Jaki jest najszybszy sposób na dokonanie tego wyboru?

Obecny algorytm wykorzystuje maskę 32-bitową z 1 w pozycji podanej przez 5 liczb i obsługiwaną przez HW funkcję CLZ.
Powinienem powiedzieć, że procesor jest zastrzeżony, niedostępny poza moją firmą. Mój kompilator to GCC, ale dostosowany do tego procesora.

Próbowałem dowiedzieć się, czy mogę użyć tabeli odnośników, ale nie udało mi się wygenerować klucza, którego mogę użyć.

Mam kombinacji dla danych wejściowych, ale kolejność nie jest ważna, tzn. Jest taka sama jak .215[5,0,0,0,5][5,5,0,0,0]

Zdarza się, że funkcja skrótu poniżej tworzy idealny skrót bez kolizji!

def hash(x):
    h = 0
    for i in x:
        h = 33*h+i
    return h

Ale skrót jest ogromny i po prostu nie ma wystarczającej ilości pamięci, aby z niego skorzystać.

Czy istnieje lepszy algorytm, którego mogę użyć? Czy jest możliwe rozwiązanie mojego problemu za pomocą tabeli przeglądowej i wygenerowania klucza?

Fredrik Pihl
źródło
1
Z jakiego algorytmu obecnie korzystasz? Wystarczy siedem porównań liczb całkowitych, czy to jest zbyt wolne? Twój hashjuż wykonuje więcej operacji. Czy kolejne wywołania metody są powiązane, np. Czy centrala xprzechodzi przez matrycę rząd po rzędzie?
Raphael
Filtr jest zawijany w obrazie rząd po rzędzie. To znaczy, uzyskaj 5 wartości i wykonaj obliczenia, a następnie przesuń wszystko o krok w prawo i powtórz. Hash był tylko przykładem. Przeprowadziłem testy porównawcze kilku rozwiązań okien przesuwnych, aby zminimalizować odczyt danych, ale wszystko sprowadza się do znalezienia najwyższych 2 wartości.
Fredrik Pihl
3
Najprawdopodobniej twój algorytm, jeśli zostanie poprawnie zaimplementowany, będzie ograniczony dostępem do pamięci, a nie obliczeniami. Korzystanie z tablicy mieszającej tylko zwiększy ilość dostępów do pamięci i spowolni działanie. Prześlij swój obecny kod, abyśmy mogli zobaczyć, jak można go ulepszyć - uważam, że możliwa jest tylko mikrooptymalizacja. Najbardziej myślę o tym: może moglibyśmy skorzystać z faktu, że 2 wartości są wspólne między sąsiednimi oknami?
jkff,
@jkff W zależności od macierzy, rozmiarów pamięci podręcznej i funkcji mapowania (pamięci podręcznej) każda wartość może wymagać jednorazowego załadowania; większość operacji powinna wtedy działać na rejestrach lub pamięci podręcznej L1. Rurociągi to jednak inna kwestia.
Raphael
1
Nawiasem mówiąc, czy robicie to już równolegle? Wydaje się, że jest to szczególnie przydatne w przypadku równoległego wektorowania lub SIMD (np. Na GPU). Ta droga pomogłaby znacznie bardziej niż zaoszczędzić kilka procent na komórkę.
Raphael

Odpowiedzi:

11

W mojej drugiej odpowiedzi sugeruję, że skoki warunkowe mogą być główną przeszkodą dla wydajności. W rezultacie przychodzą na myśl sieci sortujące : są one niezależne od danych, to znaczy ta sama sekwencja porównań jest wykonywana bez względu na dane wejściowe, przy czym tylko wymiany są warunkowe.

Oczywiście sortowanie może być zbyt pracochłonne; potrzebujemy tylko dwóch największych liczb. Na szczęście dla nas przebadano również sieci selekcyjne . Knuth mówi nam, że można znaleźć dwie najmniejsze liczby z pięciu² U^2)(5)=6 porównania [1, 5.3.4 ex 19] (i co najwyżej tyle swapów).

Sieć, którą podaje w rozwiązaniach (przepisana na tablice zerowe) to

[0:4][1:4][0:3)][1:3)][0:2)][1:2)]

który implementuje - po dostosowaniu kierunku porównań - w pseudokodzie jako

def selMax2(a : int[])
  a.swap(0,4) if a[0] < a[4]
  a.swap(1,4) if a[1] < a[4]
  a.swap(0,3) if a[0] < a[3]
  a.swap(1,3) if a[1] < a[3]
  a.swap(0,2) if a[0] < a[2]
  a.swap(1,2) if a[1] < a[2]
  return (a[0], a[1])
end

Teraz naiwne implementacje nadal mają skoki warunkowe (w całym kodzie wymiany). Jednak w zależności od maszyny można je wymyślić za pomocą instrukcji warunkowych. x86 wydaje się być zwykłym błotnistym ja; ARM wygląda bardziej obiecująco, ponieważ najwyraźniej większość operacji jest uwarunkowana sama w sobie. Jeśli dobrze rozumiem instrukcje , pierwsza zamiana przekłada się na to, zakładając, że nasze wartości tablic zostały załadowane do rejestrów R0poprzez R4:

CMP     R0,R4
MOVLT   R5 = R0
MOVLT   R0 = R4
MOVLT   R4 = R6

Tak, tak, oczywiście możesz używać zamiany XOR z EOR .

Mam tylko nadzieję, że twój procesor ma to lub coś podobnego. Oczywiście, jeśli zbudujesz coś w tym celu, może uda ci się tam podłączyć sieć?

Jest to prawdopodobnie (możliwe?) Najlepsze, co możesz zrobić w klasycznej dziedzinie, tj. Bez korzystania z ograniczonej domeny i wykonywania nikczemnych magii wewnątrz słowa.


  1. Sortowanie i wyszukiwanie według Donalda E. Knutha; The Art of Computer Programming Vol. 3 (wydanie 2, 1998)
  2. Zauważ, że pozostawia to dwa wybrane elementy nieuporządkowane. Zamawianie ich wymaga dodatkowego porównaniaW.^2)(5)=7 wielu łącznie [1, p234 Tabela 1].
Raphael
źródło
Akceptuję to Otrzymałem wiele nowych pomysłów, które muszę przetestować przed przejściem dalej. Odwoływanie się do Knuth zawsze działa dla mnie :-) Dzięki za twój wysiłek i czas!
Fredrik Pihl
@FredrikPihl Cool, daj nam znać, jak to się ostatecznie skończy!
Raphael
Będę! Przeczytaj teraz rozdział 5.3.3. Uwielbiam jego początek z odniesieniami do Lewisa Carrolla i turnieju tenisowego :-)
Fredrik Pihl
2
W zależności od zestawu instrukcji użyteczne może być użycie 2 * max (a, b) = a + b + abs (ab) wraz z siecią selekcji; może to być mniej kosztowne niż nieprzewidywalne skoki warunkowe (nawet bez wewnętrznego lub warunkowego ruchu dla abs: gcc, przynajmniej dla x86, generuje nieprzyzwoitą sekwencję, która nie wydaje się być zależna od x86). Posługiwanie się sekwencją nieprzydatną jest również przydatne w połączeniu z SIMD lub GPU.
AProgrammer
1
Zauważ, że sieci selekcyjne (jak sieci sortujące) są podatne na operacje równoległe; w szczególności w określonej sieci wyboru, porównania 1: 4 i 0: 3 mogą być wykonywane równolegle (jeśli procesor, kompilator itp. obsługują to skutecznie), a porównania 1: 3 i 0: 2 mogą być również wykonywane równolegle.
Bruce Lilly,
4

Oto algorytm bezpośredni, który znajduje się na stole:

// Sort x1, x2
if x1 < x2
  M1 = x2
  m1 = x1
else
  M1 = x1
  m1 = x2
end

// Sort x3, x4
if x3 < x4
  M2 = x4
  m2 = x3
else
  M2 = x3
  m2 = x4
end

// Pick largest two
if M1 > M2
  M3 = M1
  if m1 > M2
    m3 = m1
  else
    m3 = M2
  end
else
  M3 = M2
  if m2 > M1
    m3 = m2
  else
    m3 = M1
  end
end

// Insert x4
if x4 > M3
  m3 = M3
  M3 = x4
else if x4 > m3
  m3 = x4
end

Dzięki sprytnej implementacji if ... elsemożna pozbyć się pewnych bezwarunkowych skoków, jakie miałoby bezpośrednie tłumaczenie.

To jest brzydkie, ale zajmuje tylko

  • pięć lub sześć porównań (tj. skoki warunkowe),
  • dziewięć do dziesięciu przypisań (z 11 zmiennymi, wszystkie w rejestrach) i
  • brak dodatkowego dostępu do pamięci.

W rzeczywistości sześć porównań jest optymalnych dla tego problemu, jak pokazuje Twierdzenie S w sekcji 5.3.3 [1]; tutaj potrzebujemyW.2)(5).

Jednak nie można oczekiwać, że będzie to szybkie na maszynach z potokowaniem; biorąc pod uwagę wysoki odsetek skoków warunkowych, większość czasu prawdopodobnie spędziłaby w przeciągnięciu.

Zauważ, że prostszy wariant - rodzaj x1i x2, a następnie włóż innymi wartościami następnie - trwa od czterech do siedmiu porównań i tylko pięć do sześciu zadań. Ponieważ spodziewam się, że skoki będą tu droższe, utknąłem przy tym.


  1. Sortowanie i wyszukiwanie według Donalda E. Knutha; The Art of Computer Programming Vol. 3 (wydanie 2, 1998)
Raphael
źródło
Zastanawiam się, co kompilator optymalizacyjny może z nimi zrobić.
Raphael
Zaimplementuję to i porównam z obecnym rozwiązaniem opartym na CLZ. Dziękuję za Twój czas!
Fredrik Pihl,
1
@FredrikPihl Jaki był wynik twoich testów?
Raphael
1
Podejście oparte na SWAP pokonuje CLZ! Teraz na telefonie komórkowym. Może opublikować więcej danych innym razem, teraz na telefonie komórkowym
Fredrik Pihl
@FredrikPihl Cool! Cieszę się, że stare dobre podejście teoretyczne może (nadal) mieć praktyczne zastosowanie. :)
Raphael
4

Może to być świetna aplikacja i test dla projektu Souper . Souper to superoptimizer - narzędzie, które pobiera krótką sekwencję kodu jako dane wejściowe i próbuje zoptymalizować go w jak największym stopniu (próbuje znaleźć równoważną sekwencję kodu, która będzie szybsza).

Souper jest open source. Możesz spróbować uruchomić Souper na swoim fragmencie kodu, aby sprawdzić, czy da to coś lepszego.

Zobacz także konkurs Johna Regehra na pisanie szybkiego kodu do sortowania 16 4-bitowych wartości ; możliwe, że niektóre techniki mogą być przydatne.

DW
źródło
Byłbym zainteresowany tym, co można zrobić z programami, które próbuje OP.
Raphael
3

Możesz użyć 213)tabela, która pobiera trzy liczby całkowite i wyświetla dwie największe. Następnie możesz użyć trzech odnośników tabeli:

T[T[T[441*a+21*b+c]*21+d]*21+e]

Podobnie za pomocą 214 tabeli, możesz zredukować ją do dwóch wyszukiwań tabeli, choć nie jest jasne, że byłoby to szybsze.

Jeśli naprawdę chcesz mały stolik, możesz użyć dwóch 212)tabele, aby „posortować” dwie liczby, a następnie użyć sieci sortującej. Według Wikipedii wymaga to maksymalnie 18 przeszukiwania tabel (9 komparatorów); możesz być w stanie zrobić mniej, ponieważ (1) chcesz znać tylko dwa największe elementy, i (2) dla niektórych bramek porównawczych, możesz być zainteresowany tylko maksimum.

Możesz także użyć jednego 212)stół. Wdrożenie sieci sortującej zużywa mniej pamięci, ale więcej arytmetyki. W ten sposób uzyskuje się maksymalnie 9 odnośników do tabeli.

Yuval Filmus
źródło