Ungerade Ergebnisse bei der Auswertung eines Benchmark-Beispiels aus Rust Book

8

Benchmark-Tests aus dem Rust Book

Ich bekomme merkwürdige Ergebnisse von Mikrobenzinssätzen, die mit dem Beispiel bench_xor_1000_ints aus dem Rust Book-Kapitel auf Benchmark-Tests .

Der Abschnitt Gotcha: Optimierungen gibt allgemeine Empfehlungen, einen Wert aus dem Bencher::iter closure zurückzugeben und / oder die Funktion black_box zu verwenden. Ich endete mit diesen fünf Varianten:

Benchmarkcode

%Vor%

Benchmark-Benennung

  • Präfix xor_closure _ : Bestandener Abschluss |a, b| a ^ b
  • Präfix xor_pointer _ : Funktionszeiger übergeben xor
  • Suffix _a : b.iter closure gibt den Wert zurück (Note fehlt Semikolon)
  • suffix _b : b.iter closure gibt keinen Wert zurück (endet mit Semikolon)
  • xor_black_box : Wirf alles in black_box und hoffe auf das Beste

Benchmark-Ergebnisse

%Vor%

Einige Beobachtungen bezüglich Messungen:

  • Das Entfernen des Iterationszählers let n = black_box(1000) in der schwarzen Box und das Einfügen in den Bereich (0..1000).fold ... hat keinen Einfluss auf die Ergebnisse
  • Skalierung des Iterationszählers n skaliert die Messungen entsprechend (außer xor_pointer_b optimiert auf 0)

Insgesamt stimmen die Ergebnisse mit den allgemeinen Empfehlungen von Rust Book überein, mit Ausnahme von xor_closure_b benchmark.

Genauer gesagt scheinen Ergebnisse mit dem Suffix _a (die einen Wert zurückgeben) mit der black-boxed xor_black_box zu stimmen, was mir gut klingt. Und xor_pointer_b optimiert auf 0 scheint echt, da seine Schließung den Wert nicht zurückgibt. Aber das Ergebnis von xor_closure_b ist ungerade.

Der Assemblercode kann diese Frage klären. Wie kann ich den Rust verhindern? Benchmark-Bibliothek von der Optimierung meines Codes? gibt einen guten Überblick über das Lesen der Assembly aus den Rust-Benchmark-Tests.

%Vor%

erzeugt diese Ausgabe :

%Vor%

Wenn Sie die Assembly xor_closure_a , xor_pointer_a und xor_black_box betrachten, die alle die gleiche Leistung aufweisen, sollten Sie einen ähnlichen Assembly-Code berechnen. Das ist übrigens viel mehr Anweisungen als die Menge von ASM für arme xor_closure_b .

Hier endet meine Forschung. Ich würde mich freuen, wenn jemand erklärt, warum dieses Montageblech von xor_black_box schneller ausgewertet wird als xor_closure_b . Oder anders ausgedrückt, warum die vom Compiler weg optimierte Version deutlich langsamer läuft und welcher Benchmark-Variante ich vertrauen sollte?

Versionen

%Vor%

Aktualisieren

@Francis Gagné hat eine fantastische Arbeit geleistet, um das Problem in seiner Antwort zu untersuchen. Die Schritte, die zur Reproduktion der Ergebnisse führten, sind unten aufgeführt.

Kompilieren Sie Benchmarks und senden Sie Assembly:

%Vor%

Führen Sie die erzeugte ausführbare Datei aus, um das vorherige Verhalten zu überprüfen:

%Vor%

Kompilieren Sie die gepatchte Assembly-Ausgabe ./target/release/deps/xor.s

%Vor%

Führen Sie aktualisierte Benchmarks aus:

%Vor%     
4e6 11.09.2016, 22:34
quelle

1 Antwort

4

Ich konzentriere mich darauf, xor_closure_b und xor_pointer_b zu vergleichen, da sie eine ähnliche Leistung haben sollten (d. h. sie sollten beide nichts tun).

Update: Ich habe bei meiner ersten Analyse einen Fehler gemacht, wie von @EOF , also habe ich den folgenden Text überarbeitet.

Sehen wir uns zuerst die LLVM IR an, die für diese beiden Funktionen generiert wurde. (Ich finde LLVM IR einfacher zu lesen als ASM, weil es strukturierter ist.)

%Vor%

Wenn wir die LLVM IR für xor_closure_b und xor_pointer_b vergleichen, sehen sie ziemlich ähnlich aus. Es fällt jedoch ein Unterschied auf: Der Block bb7.i.i.i wurde aus irgendeinem Grund in xor_pointer_b , aber nicht in xor_closure_b optimiert. Hier ist der Block:

%Vor%

Und hier ist es in ASM übersetzt:

%Vor%

Dies ist ein ziemlich dummer Weg, um von 0 bis 1000 zu loopen. Ändern Sie den obigen Code zu:

%Vor%

setzt den Benchmark für xor_closure_b von 781 ns/iter (+/- 19) auf 270 ns/iter (+/- 7) auf meinem Rechner ab.

Ich kann nicht sicher sagen, warum der vom Compiler generierte Code so langsam ist oder warum er nicht von Anfang an optimiert wurde (wie in xor_pointer_b ) ... Jedoch scheint es, dass xor_pointer_a und xor_closure_a und noch schneller, weil der generierte Code vektorisiert wird, was dazu führt, dass die Schleife weniger Iterationen durchführt (dh die Schleife wird abgerollt), um einen Faktor von 32 (zB .LBB0_8 , die Hauptschleife in% co_de) %, führt 31 Iterationen durch, dann wird der Rest nach der Schleife behandelt).

Als Referenz habe ich das editierte ASM mit dieser Befehlszeile kompiliert:

  

xor_closure_a

und ich habe es mit $ gcc target/release/xor-71758a2519026d86.s ~/.multirust/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/lib{test,term,getopts,rustc_unicode,std,libc,rand,collections,alloc_system,alloc,core,panic_unwind}-411f48d3.rlib -pthread -lpthread -lm -ldl ausgeführt. Außerdem ist meine CPU ein Intel Core i7-4770K.

    
Francis Gagné 12.09.2016, 00:00
quelle