Der thread zeigt ja wunderbar, wie hoch die Qualität der Standard-Bib ist.
Einige allgemeine Ideen:
-) Ich kann noch immer nicht das
-) std::vector ist für Standardfälle spezifiziert, und nicht für das letzte Quäntchen an Performance in ganz spezifischen Situationen. In letzterem Fall kann es durchaus Sinn machen, was selbstgestricktes als Container zu verwenden.
Ein std::vector ist z.B. auch auf dynamisches Wachsen/Shrinking und einfache Handhabung ausgelegt, und während die Iteratoren prinzipiell als plain-pointers realisiert werden können, sind sie das typischerweise nicht. Sondern als eine komplexere Struktur die z.B. bei increment/decrement/de-reference auf Gültigkeit (z.B. out-of-bounds) überprüft (selbst bei optimization-on). Wenn also ein plain fixed-size array mit plain pointers als Iteratoren komplett ausreichen, dann sollte das Performance-mäßig auch besser sein als ein std::vector. Die Falle ist halt den Code dazu richtig hinzukriegen (Bsp. exception-safety).
-) Man kann durch gezielte Logik eventuell was rausholen, zwei Bsp.:
A)
Man erstelle einen std::vector<bool> oder ein boost::dynamic_bitset als Sekundärcontainer, mit Größe
In einer Schleife ziehe man nun stets Indices in [0, in.size()-1] und setzt das dazugehörige Bit im Bit-Container. Falls das Bit noch nicht gesetzt war, zählt man einen Zähler rauf, ansonsten nicht. Das ganze wird solange im rinse-&-repeat Verfahren durchgeführt (while-Schleife) bis der Zähler bei
Wenn
B)
1) x = n
2) Man ziehe x Elemente in [0, in.size()-1] und schreibst sie in einen temporären Container (mit voralloziierter Größe x)
3) Man sortiert diese gezogenen Elemente und unter Überspringen von Dupletten schreibt man diese in einen Index-Output-Container. Dieser wird nun (wegen den Dupletten) weniger als n Elemente enthalten (diese sind aber bereits sortiert, was gleich wichtig wird).
4) x = n - [Größe vom Index-Output-Container]
5) Man ziehe Elemente in [0, in.size()-1]; per std::binary_search() wird überprüft ob das gezogene Elemente bereits im Index-output-Container vorkommt (genau für das binary_search() ist es essentiell dass die Elemente darin sortiert sind!); falls ja Element verwerfen, falls nein in den (mittlerweile geleerten aber recycleten) temporären Container schreiben; solange durchführen bis x Elemente im temporären Container sind.
6) Man sortiert die gezogenen Elemente im temporären Container und unter Überspringen von Dupletten schreibt man diese wieder in den Index-Output-Container.
7) Falls der Index-Output-Container die Größe n erreicht hat, Ende; das sind die Indices deren Elemente man dann in
8) Andernfalls werden die Elemente im Index-Output-Container sortiert (notwendig für das binary_search()), dann rinse-&-repeat 4)-8).
Wie bei A) gilt: Wenn n > 50% von
EDIT: Ev. ist es besser, in 5) das std::binary_search wegzulassen und stattdessen den temporären Container einfach mit x Elementen befüllen, diese dann gemäß 6) sortieren aber beim Übertragen dieser Elemente in den Index-Output-Container nur jene übertragen die im Index-Output-Container noch nicht schon vorhanden sind. Da beide Container sortiert sind lässt sich das sehr effizient realisieren (man braucht nur einen einzigen sich vorwärts bewegenden Iterator je Container). Wichtig ist, dass das memory des Index-Output-Containers auf n Elementen vorreserviert worden ist (std::vector::reserve()), damit beim Hinzufügen von Elemente am Ende dieses Containers im Zuge dieses Kopierens der Iterator dieses Containers dabei nicht invalidiert wird.
Die Idee hier ist insbesondere, dass wenn n klein bzw. groß (letzteres durch die Reverse-Logik des Ziehens der nicht zu behaltenden Elemente) im Vergleich zu in.size() ist entsprechend weniger Zufallszahlen gezogen werden müssen und Elemente hin-und-her kopiert werden müssen als beim shuffle() oder ähnlichem. Je näher n gegen 50% geht, desto mehr wird dieser Vorteil verloren gehen, und dann könnte es Sinn machen auf die shuffle()-Logik umzusteigen.
Bei 5% zu ziehenden Elemente würde ich erwarten, dass obige Variante bedeutsam schneller ist als die shuffle-Version.
Am besten wird es sein, mal die Implementation von std::sample() anzuschauen was die macht weswege sie so effizient ist. Würde mich auch gar nicht wundern, wenn die je nach der Proportion der zu ziehenden Elemente ebenfalls jeweils andere Logiken verwendet (die Sortiertalgorithmen machen sowas ja auch und switchen dynamisch um jenachdem wie sehr die Elemente schon sortiert wirken bzw. nicht, etc.).
Einige allgemeine Ideen:
-) Ich kann noch immer nicht das
reserve() für den vector finden, obwohl das jetzt schon unzählige Male genannt worden ist.-) std::vector ist für Standardfälle spezifiziert, und nicht für das letzte Quäntchen an Performance in ganz spezifischen Situationen. In letzterem Fall kann es durchaus Sinn machen, was selbstgestricktes als Container zu verwenden.
Ein std::vector ist z.B. auch auf dynamisches Wachsen/Shrinking und einfache Handhabung ausgelegt, und während die Iteratoren prinzipiell als plain-pointers realisiert werden können, sind sie das typischerweise nicht. Sondern als eine komplexere Struktur die z.B. bei increment/decrement/de-reference auf Gültigkeit (z.B. out-of-bounds) überprüft (selbst bei optimization-on). Wenn also ein plain fixed-size array mit plain pointers als Iteratoren komplett ausreichen, dann sollte das Performance-mäßig auch besser sein als ein std::vector. Die Falle ist halt den Code dazu richtig hinzukriegen (Bsp. exception-safety).
-) Man kann durch gezielte Logik eventuell was rausholen, zwei Bsp.:
A)
Man erstelle einen std::vector<bool> oder ein boost::dynamic_bitset als Sekundärcontainer, mit Größe
in.size(). Jedes Element darin mappt natürlich zum entsprechenden Element in in.In einer Schleife ziehe man nun stets Indices in [0, in.size()-1] und setzt das dazugehörige Bit im Bit-Container. Falls das Bit noch nicht gesetzt war, zählt man einen Zähler rauf, ansonsten nicht. Das ganze wird solange im rinse-&-repeat Verfahren durchgeführt (while-Schleife) bis der Zähler bei
n angelangt ist, das sind dann die zu behaltenden Elemente, und die kopiert man dann in den Output-Container.Wenn
n > 50% von in.size() ist, dann werden in.size()-n Indices gezogen und diese markieren dann die nicht zu behaltenden Elemente.B)
1) x = n
2) Man ziehe x Elemente in [0, in.size()-1] und schreibst sie in einen temporären Container (mit voralloziierter Größe x)
3) Man sortiert diese gezogenen Elemente und unter Überspringen von Dupletten schreibt man diese in einen Index-Output-Container. Dieser wird nun (wegen den Dupletten) weniger als n Elemente enthalten (diese sind aber bereits sortiert, was gleich wichtig wird).
4) x = n - [Größe vom Index-Output-Container]
5) Man ziehe Elemente in [0, in.size()-1]; per std::binary_search() wird überprüft ob das gezogene Elemente bereits im Index-output-Container vorkommt (genau für das binary_search() ist es essentiell dass die Elemente darin sortiert sind!); falls ja Element verwerfen, falls nein in den (mittlerweile geleerten aber recycleten) temporären Container schreiben; solange durchführen bis x Elemente im temporären Container sind.
6) Man sortiert die gezogenen Elemente im temporären Container und unter Überspringen von Dupletten schreibt man diese wieder in den Index-Output-Container.
7) Falls der Index-Output-Container die Größe n erreicht hat, Ende; das sind die Indices deren Elemente man dann in
out schreibt.8) Andernfalls werden die Elemente im Index-Output-Container sortiert (notwendig für das binary_search()), dann rinse-&-repeat 4)-8).
Wie bei A) gilt: Wenn n > 50% von
in.size() ist, sind die gezogenen Indices diejenigen für die nicht zu behaltenden Elemente.EDIT: Ev. ist es besser, in 5) das std::binary_search wegzulassen und stattdessen den temporären Container einfach mit x Elementen befüllen, diese dann gemäß 6) sortieren aber beim Übertragen dieser Elemente in den Index-Output-Container nur jene übertragen die im Index-Output-Container noch nicht schon vorhanden sind. Da beide Container sortiert sind lässt sich das sehr effizient realisieren (man braucht nur einen einzigen sich vorwärts bewegenden Iterator je Container). Wichtig ist, dass das memory des Index-Output-Containers auf n Elementen vorreserviert worden ist (std::vector::reserve()), damit beim Hinzufügen von Elemente am Ende dieses Containers im Zuge dieses Kopierens der Iterator dieses Containers dabei nicht invalidiert wird.
Die Idee hier ist insbesondere, dass wenn n klein bzw. groß (letzteres durch die Reverse-Logik des Ziehens der nicht zu behaltenden Elemente) im Vergleich zu in.size() ist entsprechend weniger Zufallszahlen gezogen werden müssen und Elemente hin-und-her kopiert werden müssen als beim shuffle() oder ähnlichem. Je näher n gegen 50% geht, desto mehr wird dieser Vorteil verloren gehen, und dann könnte es Sinn machen auf die shuffle()-Logik umzusteigen.
Bei 5% zu ziehenden Elemente würde ich erwarten, dass obige Variante bedeutsam schneller ist als die shuffle-Version.
Am besten wird es sein, mal die Implementation von std::sample() anzuschauen was die macht weswege sie so effizient ist. Würde mich auch gar nicht wundern, wenn die je nach der Proportion der zu ziehenden Elemente ebenfalls jeweils andere Logiken verwendet (die Sortiertalgorithmen machen sowas ja auch und switchen dynamisch um jenachdem wie sehr die Elemente schon sortiert wirken bzw. nicht, etc.).
Zuletzt bearbeitet: