Der schnellste Weg, um 10 Zahlen zu sortieren? (Zahlen sind 32 Bit)


211

Ich löse ein Problem und es geht darum, 10 Zahlen (int32) sehr schnell zu sortieren. Meine Anwendung muss 10 Zahlen millionenfach so schnell wie möglich sortieren. Ich probiere einen Datensatz mit Milliarden von Elementen aus und jedes Mal muss ich 10 Zahlen daraus auswählen (vereinfacht) und sortieren (und aus der Liste der sortierten 10 Elemente Schlussfolgerungen ziehen).

Derzeit verwende ich die Einfügesortierung, aber ich kann mir vorstellen, dass ich einen sehr schnellen benutzerdefinierten Sortieralgorithmus für mein spezifisches Problem von 10 Zahlen implementieren könnte, der die Einfügungssortierung übertreffen würde.

Hat jemand eine Idee, wie man dieses Problem angeht?


14
So grob es auch klingen mag, eine Reihe verschachtelter ifAnweisungen sollte am besten funktionieren. Vermeiden Sie Schleifen.
Ja72

8
Erwarten Sie, dass Ihnen die Zahlen mit einer Verzerrung in der Menge der Permutationen gegeben werden, oder werden sie gleichmäßig verteilt sein? Wird es einen Zusammenhang zwischen der Reihenfolge einer Liste und der nächsten geben?
Douglas Zare

4
Der gesamte Datensatz (mit Milliarden von Zahlen) wird gemäß dem Gesetz von Benford verteilt, aber wenn ich zufällig Elemente aus diesem Satz auswähle, sind sie es nicht mehr (glaube ich).
Bodacydo

13
Vielleicht möchten Sie diesen stackoverflow.com/q/2786899/995714
phuclv

11
Wenn Sie zufällig aus Milliarden von Elementen auswählen, ist es durchaus möglich, dass die Latenz zum Abrufen dieser Daten einen größeren Einfluss hat als die Zeit, die zum Sortieren der ausgewählten Elemente erforderlich ist, selbst wenn sich der gesamte Datensatz im RAM befindet. Sie können die Auswirkungen testen, indem Sie die Leistung vergleichen und die Daten nacheinander oder nach dem Zufallsprinzip auswählen.
Steve S.

Antworten:


213

(Befolgen Sie den Vorschlag von HelloWorld, sich mit dem Sortieren von Netzwerken zu befassen.)

Es scheint, dass ein 29-Vergleichs- / Swap-Netzwerk der schnellste Weg ist, eine Sortierung mit 10 Eingängen durchzuführen. Ich habe das 1969 von Waksman entdeckte Netzwerk für dieses Beispiel in Javascript verwendet, das direkt in C übersetzt werden sollte, da es nur eine Liste von ifAussagen, Vergleichen und Swaps ist.

function sortNet10(data) {	// ten-input sorting network by Waksman, 1969
    var swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
    return(data);
}

alert(sortNet10([5,7,1,8,4,3,6,9,2,0]));

Hier ist eine grafische Darstellung des Netzwerks, unterteilt in unabhängige Phasen. Um die Parallelverarbeitung zu nutzen, kann die 5-4-3-4-4-4-3-2-Gruppierung in eine 4-4-4-4-4-4-3-2-Gruppierung geändert werden.
Sortiernetzwerk mit 10 Eingängen (Waksman, 1969)

Sortiernetzwerk mit 10 Eingängen (Waksman, 1969) neu gruppiert


69
Vorschlag; Verwenden Sie ein Swap-Makro. wie#define SORTPAIR(data, i1, i2) if (data[i1] > data[i2]) { int swap = data[i1]... }
Peter Cordes

9
Kann logisch gezeigt werden, dass dies das Minimum ist?
CorsiKa

8
@corsiKa Ja, Sortiernetzwerke sind seit den Anfängen der Informatik ein Forschungsgebiet. In vielen Fällen sind seit Jahrzehnten optimale Lösungen bekannt. Siehe en.wikipedia.org/wiki/Sorting_network
m69 '' snarky und unwillkommen ''

8
Ich habe einen Jsperf zum Testen erstellt und kann bestätigen, dass die Netzwerksortierung mehr als 20-mal schneller ist als die native Sortierung des Browsers. jsperf.com/fastest-10-number-sort
Daniel

9
@Katai Dies würde jede Optimierung zerstören, die Ihr Compiler möglicherweise erzeugt. Schlechte Idee. Lesen Sie dies für weitere Informationen en.wikipedia.org/wiki/…
Antzi

88

Wenn Sie mit dieser festen Größe arbeiten, schauen Sie sich Sorting Networks an . Diese Algorithmen haben eine feste Laufzeit und sind unabhängig von ihrer Eingabe. Für Ihren Anwendungsfall haben Sie keinen solchen Overhead, den einige Sortieralgorithmen haben.

Die bitonische Sortierung ist eine Implementierung eines solchen Netzwerks. Dieser funktioniert am besten mit len ​​(n) <= 32 auf einer CPU. Bei größeren Eingängen könnte man sich vorstellen, auf eine GPU umzusteigen. https://en.wikipedia.org/wiki/Sorting_network

Übrigens, eine gute Seite zum Vergleichen von Sortieralgorithmen ist diese hier (obwohl die fehlt bitonic sort.

http://www.sorting-algorithms.com


3
@ ErickG.Hagstrom Es gibt viele Lösungen; Solange sie 29 Vergleiche verwenden, sind sie gleich effizient. Ich habe Waksmans Lösung von 1969 verwendet. Er war anscheinend der erste, der eine Version mit 29 Vergleichen entdeckte.
m69 '' snarky und unwillkommen ''

1
Ja, @ m69. Es gibt über eine Million. Waksmans Lösung hat eine Länge von 29 und eine Tiefe von 9. Die von mir verknüpfte Lösung ist eine Verbesserung gegenüber der Tiefendimension: Länge = 29, Tiefe = 8. Bei der Implementierung in C spielt die Tiefe natürlich keine Rolle.
Erick G. Hagstrom

4
@ ErickG.Hagstrom Anscheinend gibt es 87 Lösungen mit Tiefe 7, von denen die erste 1973 von Knuth gefunden wurde, aber ich konnte mit einem schnellen Google keine finden. larc.unt.edu/ian/pubs/9-input.pdf (siehe Schlussfolgerung, S.14)
m69 '' snarky und unwillkommen ''

4
@ ErickG.Hagstrom: Die Tiefe macht möglicherweise "auf C-Ebene" keinen Unterschied, aber vermutlich besteht nach Abschluss des Compilers und der CPU die Möglichkeit, dass sie teilweise innerhalb der CPU parallelisiert wird, und daher könnte eine geringere Tiefe hilfreich sein. Abhängig von der CPU natürlich: Einige CPUs sind relativ einfach und erledigen eine Sache nach der anderen, während einige CPUs mehrere Operationen im Flug ausführen können. Insbesondere kann es sein, dass Sie für alle Lasten und Speicher des Stacks, in denen sie benötigt werden, eine sehr unterschiedliche Leistung erzielen um 10 Variablen zu manipulieren, je nachdem, wie sie gemacht werden.
Steve Jessop

1
@ ErickG.Hagstrom Aus dem Artikel von Ian Parberry ging nicht sofort hervor, aber die Tiefen-7-Netzwerke haben eine Länge von mehr als 29. Siehe Knuth, "The Art Of Computer Programming Vol.III", §5.3.4, Abb . 49 und 51.
m69 '' snarky und unwillkommen ''

33

Verwenden Sie ein Sortiernetzwerk mit Vergleichen in 4er-Gruppen, damit Sie dies in SIMD-Registern tun können. Ein Paar gepackter Min / Max-Anweisungen implementiert eine gepackte Komparatorfunktion. Es tut mir leid, dass ich momentan keine Zeit habe, nach einer Seite zu suchen, von der ich mich erinnere, dass ich sie gesehen habe, aber hoffentlich wird die Suche in SIMD- oder SSE-Sortiernetzwerken etwas ergeben.

x86 SSE verfügt über gepackte 32-Bit-Integer-Min- und Max-Befehle für Vektoren mit vier 32-Bit-Ints. AVX2 (Haswell und höher) hat das gleiche, jedoch für 256b-Vektoren von 8 Zoll. Es gibt auch effiziente Shuffle-Anweisungen.

Wenn Sie viele unabhängige kleine Sortierungen haben, können möglicherweise 4 oder 8 Sortierungen parallel mit Vektoren durchgeführt werden. Esp. Wenn Sie Elemente nach dem Zufallsprinzip auswählen (damit die zu sortierenden Daten ohnehin nicht zusammenhängend im Speicher sind), können Sie das Mischen vermeiden und einfach in der gewünschten Reihenfolge vergleichen. 10 Register für alle Daten aus 4 (AVX2: 8) Listen mit 10 Ints lassen noch 6 Register für den Arbeitsbereich übrig.

Vektorsortiernetzwerke sind weniger effizient, wenn Sie auch zugehörige Daten sortieren müssen. In diesem Fall scheint der effizienteste Weg darin zu bestehen, einen gepackten Vergleich zu verwenden, um eine Maske zu erhalten, deren Elemente geändert wurden, und diese Maske zu verwenden, um Vektoren von (Verweisen auf) zugeordneten Daten zu mischen.


26

Was ist mit einer ungerollten, verzweigungslosen Auswahlsorte?

#include <iostream>
#include <algorithm>
#include <random>

//return the index of the minimum element in array a
int min(const int * const a) {
  int m = a[0];
  int indx = 0;
  #define TEST(i) (m > a[i]) && (m = a[i], indx = i ); 
  //see http://stackoverflow.com/a/7074042/2140449
  TEST(1);
  TEST(2);
  TEST(3);
  TEST(4);
  TEST(5);
  TEST(6);
  TEST(7);
  TEST(8);
  TEST(9);
  #undef TEST
  return indx;
}

void sort( int * const a ){
  int work[10];
  int indx;
  #define GET(i) indx = min(a); work[i] = a[indx]; a[indx] = 2147483647; 
  //get the minimum, copy it to work and set it at max_int in a
  GET(0);
  GET(1);
  GET(2);
  GET(3);
  GET(4);
  GET(5);
  GET(6);
  GET(7);
  GET(8);
  GET(9);
  #undef GET
  #define COPY(i) a[i] = work[i];
  //copy back to a
  COPY(0);
  COPY(1);
  COPY(2);
  COPY(3);
  COPY(4);
  COPY(5);
  COPY(6);
  COPY(7);
  COPY(8);
  COPY(9);
  #undef COPY
}

int main() {
  //generating and printing a random array
  int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
  std::random_device rd;
  std::mt19937 g(rd());
  std::shuffle( a, a+10, g);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  }
  std::cout << std::endl;

  //sorting and printing again
  sort(a);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  } 

  return 0;
}

http://coliru.stacked-crooked.com/a/71e18bc4f7fa18c6

Die einzigen relevanten Zeilen sind die ersten beiden #define.

Es verwendet zwei Listen und überprüft die erste zehnmal vollständig, was eine schlecht implementierte Auswahlsortierung wäre. Es werden jedoch Verzweigungen und Schleifen variabler Länge vermieden, die mit modernen Prozessoren und einem so kleinen Datensatz kompensiert werden können.


Benchmark

Ich habe einen Vergleich mit dem Sortiernetzwerk durchgeführt, und mein Code scheint langsamer zu sein. Ich habe jedoch versucht, das Abrollen und die Kopie zu entfernen. Ausführen dieses Codes:

#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>

int min(const int * const a, int i) {
  int m = a[i];
  int indx = i++;
  for ( ; i<10; i++) 
    //see http://stackoverflow.com/a/7074042/2140449
    (m > a[i]) && (m = a[i], indx = i ); 
  return indx;
}

void sort( int * const a ){
  for (int i = 0; i<9; i++)
    std::swap(a[i], a[min(a,i)]); //search only forward
}


void sortNet10(int * const data) {  // ten-input sorting network by Waksman, 1969
    int swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
}


std::chrono::duration<double> benchmark( void(*func)(int * const), const int seed ) {
  std::mt19937 g(seed);
  int a[10] = {10,11,12,13,14,15,16,17,18,19};
  std::chrono::high_resolution_clock::time_point t1, t2; 
  t1 = std::chrono::high_resolution_clock::now();
  for (long i = 0; i < 1e7; i++) {
    std::shuffle( a, a+10, g);
    func(a);
  }
  t2 = std::chrono::high_resolution_clock::now();
  return std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
}

int main() {
  std::random_device rd;
  for (int i = 0; i < 10; i++) {
    const int seed = rd();
    std::cout << "seed = " << seed << std::endl;
    std::cout << "sortNet10: " << benchmark(sortNet10, seed).count() << std::endl;
    std::cout << "sort:      " << benchmark(sort,      seed).count() << std::endl;
  }
  return 0;
}

Ich erhalte durchweg ein besseres Ergebnis für die verzweigungslose Auswahlsortierung im Vergleich zum Sortiernetzwerk.

$ gcc -v
gcc version 5.2.0 (GCC) 
$ g++ -std=c++11 -Ofast sort.cpp && ./a.out
seed = -1727396418
sortNet10: 2.24137
sort:      2.21828
seed = 2003959850
sortNet10: 2.23914
sort:      2.21641
seed = 1994540383
sortNet10: 2.23782
sort:      2.21778
seed = 1258259982
sortNet10: 2.25199
sort:      2.21801
seed = 1821086932
sortNet10: 2.25535
sort:      2.2173
seed = 412262735
sortNet10: 2.24489
sort:      2.21776
seed = 1059795817
sortNet10: 2.29226
sort:      2.21777
seed = -188551272
sortNet10: 2.23803
sort:      2.22996
seed = 1043757247
sortNet10: 2.2503
sort:      2.23604
seed = -268332483
sortNet10: 2.24455
sort:      2.24304

4
Die Ergebnisse sind nicht sehr beeindruckend, aber tatsächlich das, was ich erwartet hätte. Das Sortiernetzwerk minimiert Vergleiche und keine Swaps. Wenn sich alle Werte bereits im Cache befinden, sind Vergleiche viel billiger als Swaps, sodass eine Auswahlsortierung (die die Anzahl der Swaps minimiert) die Oberhand hat. (und es gibt nicht so viele weitere Vergleiche: Netzwerk mit 29 Kompasionen, bis zu 29 Swaps?; vs. Auswahlsortierung mit 45 Vergleichen und höchstens 9 Swaps)
Beispiel

7
Oh, und es hat Verzweigungen - es sei denn, die Linie for ( ; i<10; i++) (m > a[i]) && (m = a[i], indx = i ); ist außergewöhnlich gut optimiert. (Kurzschluss ist normalerweise eine Form der Verzweigung)
Beispiel

1
@EugeneRyabtsev das auch, aber es wird immer mit genau den gleichen zufälligen Sequenzen gespeist, so dass es abbrechen sollte. Ich habe versucht , zu ändern , std::shufflemit for (int n = 0; n<10; n++) a[n]=g();. Die Ausführungszeit halbiert sich und das Netzwerk ist jetzt schneller.
DarioP

Wie ist das mit libc ++ zu vergleichen std::sort?
Gnzlbg

1
@gnzlbg Ich habe es auch versucht, std::sortaber es lief so schlecht, dass ich es nicht einmal in den Benchmark aufgenommen habe. Ich denke, dass mit winzigen Datensätzen ein ziemlicher Overhead verbunden ist.
DarioP

20

Die Frage besagt nicht, dass dies eine Art webbasierte Anwendung ist. Das einzige, was mir aufgefallen ist, war:

Ich probiere einen Datensatz mit Milliarden von Elementen aus und jedes Mal muss ich 10 Zahlen daraus auswählen (vereinfacht) und sortieren (und aus der Liste der sortierten 10 Elemente Schlussfolgerungen ziehen).

Als Software- und Hardware-Ingenieur schreit mir das absolut "FPGA" zu. Ich weiß nicht, welche Art von Schlussfolgerungen Sie aus dem sortierten Satz von Zahlen ziehen müssen oder woher die Daten stammen, aber ich weiß, dass es fast trivial wäre, irgendwo zwischen hundert Millionen und einer Milliarde dieser "Sort-and-" zu verarbeiten. analysieren "Operationen pro Sekunde . Ich habe in der Vergangenheit FPGA-gestützte DNA-Sequenzierungsarbeiten durchgeführt. Es ist fast unmöglich, die enorme Rechenleistung von FPGAs zu übertreffen, wenn das Problem für diese Art von Lösung gut geeignet ist.

In gewisser Weise ist der einzige einschränkende Faktor, wie schnell Sie Daten in ein FPGA schaufeln und wie schnell Sie sie herausholen können.

Als Referenz habe ich einen Hochleistungs-Echtzeit-Bildprozessor entwickelt, der 32-Bit-RGB-Bilddaten mit einer Rate von etwa 300 Millionen Pixel pro Sekunde empfängt. Die Daten werden durch FIR-Filter, Matrixmultiplikatoren, Nachschlagetabellen, räumliche Kantenerkennungsblöcke und eine Reihe anderer Operationen gestreamt, bevor sie am anderen Ende herauskommen. All dies auf einem relativ kleinen Xilinx Virtex2-FPGA mit interner Taktung von etwa 33 MHz bis, wenn ich mich richtig erinnere, 400 MHz. Oh ja, es hatte auch eine DDR2-Controller-Implementierung und zwei Bänke mit DDR2-Speicher.

Ein FPGA kann bei jedem Taktübergang eine Art von zehn 32-Bit-Zahlen ausgeben, während es mit Hunderten von MHz arbeitet. Zu Beginn des Vorgangs würde es eine kurze Verzögerung geben, wenn die Daten die Verarbeitungspipeline (n) füllen. Danach sollten Sie in der Lage sein, ein Ergebnis pro Uhr zu erhalten. Oder mehr, wenn die Verarbeitung durch Replizieren der Sortier- und Analyse-Pipeline parallelisiert werden kann. Die Lösung ist im Prinzip fast trivial.

Der Punkt ist: Wenn die Anwendung nicht an einen PC gebunden ist und der Datenstrom und die Verarbeitung mit einer FPGA-Lösung (entweder eigenständig oder als Co-Prozessor-Karte in der Maschine) "kompatibel" sind, gibt es keine Möglichkeit in der Lage zu sein, das erreichbare Leistungsniveau mit Software zu übertreffen, die in einer beliebigen Sprache geschrieben ist, unabhängig vom Algorithmus.

BEARBEITEN:

Ich habe gerade eine schnelle Suche durchgeführt und ein Papier gefunden, das für Sie von Nutzen sein könnte. Es sieht so aus, als ob es aus dem Jahr 2012 stammt. Sie können heute (und sogar damals) eine VIEL bessere Leistung erzielen. Hier ist es:

Sortieren von Netzwerken auf FPGAs


10

Ich habe kürzlich eine kleine Klasse geschrieben , die den Bose-Nelson-Algorithmus verwendet, um beim Kompilieren ein Sortiernetzwerk zu generieren.

Es kann verwendet werden, um eine sehr schnelle Sortierung für 10 Zahlen zu erstellen.

/**
 * A Functor class to create a sort for fixed sized arrays/containers with a
 * compile time generated Bose-Nelson sorting network.
 * \tparam NumElements  The number of elements in the array or container to sort.
 * \tparam T            The element type.
 * \tparam Compare      A comparator functor class that returns true if lhs < rhs.
 */
template <unsigned NumElements, class Compare = void> class StaticSort
{
    template <class A, class C> struct Swap
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            T t = Compare()(v0, v1) ? v0 : v1; // Min
            v1 = Compare()(v0, v1) ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A> struct Swap <A, void>
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            // Explicitly code out the Min and Max to nudge the compiler
            // to generate branchless code.
            T t = v0 < v1 ? v0 : v1; // Min
            v1 = v0 < v1 ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A, class C, int I, int J, int X, int Y> struct PB
    {
        inline PB(A &a)
        {
            enum { L = X >> 1, M = (X & 1 ? Y : Y + 1) >> 1, IAddL = I + L, XSubL = X - L };
            PB<A, C, I, J, L, M> p0(a);
            PB<A, C, IAddL, J + M, XSubL, Y - M> p1(a);
            PB<A, C, IAddL, J, XSubL, M> p2(a);
        }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 1>
    {
        inline PB(A &a) { Swap<A, C> s(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 2>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J); Swap<A, C> s1(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 2, 1>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J - 1); Swap<A, C> s1(a, I, J - 1); }
    };

    template <class A, class C, int I, int M, bool Stop = false> struct PS
    {
        inline PS(A &a)
        {
            enum { L = M >> 1, IAddL = I + L, MSubL = M - L};
            PS<A, C, I, L, (L <= 1)> ps0(a);
            PS<A, C, IAddL, MSubL, (MSubL <= 1)> ps1(a);
            PB<A, C, I, IAddL, L, MSubL> pb(a);
        }
    };

    template <class A, class C, int I, int M> struct PS <A, C, I, M, true>
    {
        inline PS(A &a) {}
    };

public:
    /**
     * Sorts the array/container arr.
     * \param  arr  The array/container to be sorted.
     */
    template <class Container> inline void operator() (Container &arr) const
    {
        PS<Container, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };

    /**
     * Sorts the array arr.
     * \param  arr  The array to be sorted.
     */
    template <class T> inline void operator() (T *arr) const
    {
        PS<T*, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };
};

#include <iostream>
#include <vector>

int main(int argc, const char * argv[])
{
    enum { NumValues = 10 };

    // Arrays
    {
        int rands[NumValues];
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    std::cout << "\n";

    // STL Vector
    {
        std::vector<int> rands(NumValues);
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    return 0;
}

Beachten Sie, dass if (compare) swapwir anstelle einer Anweisung explizit ternäre Operatoren für min und max codieren. Dies soll dem Compiler helfen, verzweigungslosen Code zu verwenden.

Benchmarks

Die folgenden Benchmarks wurden mit clang -O3 kompiliert und auf meinem Macbook Air Mitte 2012 ausgeführt.

Zufällige Daten sortieren

Im Vergleich zum DarioP-Code ist hier die Anzahl der Millisekunden angegeben, die zum Sortieren von 1 Million 32-Bit-Int-Arrays der Größe 10 benötigt werden:

Hardcoded Sort Net 10: 88.774 ms
Templated Bose-Nelson sort 10: 27.815 ms

Mit diesem Vorlagenansatz können wir beim Kompilieren auch Sortiernetzwerke für eine andere Anzahl von Elementen generieren.

Zeit (in Millisekunden) zum Sortieren von 1 Million Arrays unterschiedlicher Größe.
Die Anzahl der Millisekunden für Arrays der Größe 2, 4, 8 beträgt 1,943, 8,655 bzw. 20,246.
Cose Template Bose-Nelson Static Sort Timings

Dank an Glenn Teitelbaum für die abgewickelte Einfügungssorte.

Hier sind die durchschnittlichen Uhren pro Sortierung für kleine Arrays mit 6 Elementen. Der Benchmark-Code und die Beispiele finden Sie bei dieser Frage:
Schnellste Art von 6-int-Array mit fester Länge

Direct call to qsort library function       : 326.81
Naive implementation (insertion sort)       : 132.98
Insertion Sort (Daniel Stutzbach)           : 104.04
Insertion Sort Unrolled                     : 99.64
Insertion Sort Unrolled (Glenn Teitelbaum)  : 81.55
Rank Order                                  : 44.01
Rank Order with registers                   : 42.40
Sorting Networks (Daniel Stutzbach)         : 88.06
Sorting Networks (Paul R)                   : 31.64
Sorting Networks 12 with Fast Swap          : 29.68
Sorting Networks 12 reordered Swap          : 28.61
Reordered Sorting Network w/ fast swap      : 24.63
Templated Sorting Network (this class)      : 25.37

Es ist so schnell wie das schnellste Beispiel in der Frage für 6 Elemente.

Leistung zum Sortieren sortierter Daten

Oft sind die Eingabearrays bereits oder größtenteils sortiert.
In solchen Fällen kann die Einfügesortierung die bessere Wahl sein.

Geben Sie hier die Bildbeschreibung ein

Abhängig von den Daten möchten Sie möglicherweise einen geeigneten Sortieralgorithmus auswählen.

Den für die Benchmarks verwendeten Code finden Sie hier .


Gibt es eine Chance, dass Sie unten einen Vergleich für mein Algo hinzufügen können?
Glenn Teitelbaum

@GlennTeitelbaum Gibt es eine Chance, dass Sie dies zu Ihren Benchmarks hinzugefügt und Mittel und Ergebnisse offengelegt haben?
Graubart

Ein großes Lob für das Hinzufügen von Daten zum Sortieren sortierter Eingaben.
Graubart

Auf einigen Systemen v1 = v0 < v1 ? v1 : v0; // Maxkann sich noch verzweigen, in diesem Fall kann es durch ersetzt werden, v1 += v0 - tdenn wenn tes v0dann v1 + v0 -t == v1 + v0 - v0 == v1anders tist v1undv1 + v0 -t == v1 + v0 - v1 == v0
Glenn Teitelbaum

Das Ternär wird normalerweise zu einem maxssoder einer minssAnweisung für moderne Compiler kompiliert. In Fällen, in denen dies nicht funktioniert, können andere Arten des Austauschs verwendet werden. :)
Vektorisiert

5

Obwohl eine Netzwerksortierung gute Chancen hat, auf kleinen Arrays schnell zu sein, können Sie die Einfügesortierung manchmal nicht übertreffen, wenn sie richtig optimiert ist. Zum Beispiel Batch-Insert mit 2 Elementen:

{
    final int a=in[0]<in[1]?in[0]:in[1];
    final int b=in[0]<in[1]?in[1]:in[0];
    in[0]=a;
    in[1]=b;
}
for(int x=2;x<10;x+=2)
{
    final int a=in[x]<in[x+1]?in[x]:in[x+1];
    final int b=in[x]<in[x+1]?in[x+1]:in[x];
    int y= x-1;

    while(y>=0&&in[y]>b)
    {
        in[y+2]= in[y];
        --y;
    }
    in[y+2]=b;
    while(y>=0&&in[y]>a)
    {
        in[y+1]= in[y];
        --y;
    }
    in[y+1]=a;
}

Nicht sicher, warum Sie wiederholen in[y+2]= in[y];, Tippfehler?
Glenn Teitelbaum

Wow, wie habe ich das gemacht? Und wie hat es so lange gedauert, bis jemand es bemerkt hat? Die Antwort: Es ist kein Tippfehler: Ich habe einen anderen Algorithmus angepasst, der sowohl einen Schlüssel als auch ein Wertearray hatte.
Warren

3

Sie können sich vollständig abrollen insertion sort

Um dies zu vereinfachen, können rekursive templates ohne Funktionsaufwand verwendet werden. Da es bereits ein ist template, intkann es auch ein templateParameter sein. Dies macht es auch trivial, andere Array-Größen als 10 zu erstellen.

Beachten Sie, dass zum Sortieren int x[10]des Aufrufs insert_sort<int, 9>::sort(x);die Klasse den Index des letzten Elements verwendet. Dies könnte verpackt werden, aber das wäre mehr Code zum Durchlesen.

template <class T, int NUM>
class insert_sort;

template <class T>
class insert_sort<T,0>
// stop template recursion
// sorting 1 item is a no-op
{
public:
    static void place(T *x) {}
    static void sort(T * x) {}
};

template <class T, int NUM>
class insert_sort
// use template recursion to do insertion sort
// NUM is the index of the last item, eg. for x[10] call <9>
{
public:
    static void place(T *x)
    {
        T t1=x[NUM-1];
        T t2=x[NUM];
        if (t1 > t2)
        {
            x[NUM-1]=t2;
            x[NUM]=t1;
            insert_sort<T,NUM-1>::place(x);
        }
    }
    static void sort(T * x)
    {
        insert_sort<T,NUM-1>::sort(x); // sort everything before
        place(x);                    // put this item in
    }
};

In meinen Tests war dies schneller als die Beispiele für das Sortiernetzwerk.


0

Aus ähnlichen Gründen wie den hier beschriebenen funktionieren die folgenden Sortierfunktionen sort6_iterator()und sort10_iterator_local()sollten gut funktionieren , wenn das Sortiernetzwerk von hier übernommen wurde :

template<class IterType> 
inline void sort10_iterator(IterType it) 
{
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   auto data##a=*(data+a);
#define DD2(a,b) auto data##a=*(data+a), data##b=*(data+b);
#define CB1(a)   *(data+a)=data##a;
#define CB2(a,b) *(data+a)=data##a;*(data+b)=data##b;
  DD2(1,4) SORT2(1,4) DD2(7,8) SORT2(7,8) DD2(2,3) SORT2(2,3) DD2(5,6) SORT2(5,6) DD2(0,9) SORT2(0,9) 
  SORT2(2,5) SORT2(0,7) SORT2(8,9) SORT2(3,6) 
  SORT2(4,9) SORT2(0,1) 
  SORT2(0,2) CB1(0) SORT2(6,9) CB1(9) SORT2(3,5) SORT2(4,7) SORT2(1,8) 
  SORT2(3,4) SORT2(5,8) SORT2(6,7) SORT2(1,2) 
  SORT2(7,8) CB1(8) SORT2(1,3) CB1(1) SORT2(2,5) SORT2(4,6) 
  SORT2(2,3) CB1(2) SORT2(6,7) CB1(7) SORT2(4,5) 
  SORT2(3,4) CB2(3,4) SORT2(5,6) CB2(5,6) 
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

Um diese Funktion aufzurufen, habe ich einen std::vectorIterator übergeben.


0

Eine Einfügungssortierung erfordert durchschnittlich 29,6 Vergleiche, um 10 Eingaben mit einem besten Fall von 9 und einem schlechtesten von 45 zu sortieren (bei einer Eingabe in umgekehrter Reihenfolge).

Eine {9,6,1} Shellsort erfordert durchschnittlich 25,5 Vergleiche, um 10 Eingaben zu sortieren. Der beste Fall sind 14 Vergleiche, der schlechteste 34 und das Sortieren einer umgekehrten Eingabe erfordert 22.

Die Verwendung von Shellsort anstelle von Insertionssortierung reduziert den durchschnittlichen Fall um 14%. Obwohl der beste Fall um 56% erhöht wird, wird der schlechteste Fall um 24% reduziert, was bei Anwendungen von Bedeutung ist, bei denen es wichtig ist, die Leistung im schlechtesten Fall in Schach zu halten. Der umgekehrte Fall wird um 51% reduziert.

Da Sie mit der Einfügungssortierung vertraut zu sein scheinen, können Sie den Algorithmus als Sortiernetzwerk für {9,6} implementieren und anschließend die Einfügungssortierung ({1}) aktivieren:

i[0] with i[9]    // {9}

i[0] with i[6]    // {6}
i[1] with i[7]    // {6}
i[2] with i[8]    // {6}
i[3] with i[9]    // {6}

i[0 ... 9]        // insertion sort
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.