Ich habe eine interessante Leistungsregression in einem kleinen C ++ - Snippet gefunden, wenn ich C ++ 11 aktiviere:
#include <vector>
struct Item
{
int a;
int b;
};
int main()
{
const std::size_t num_items = 10000000;
std::vector<Item> container;
container.reserve(num_items);
for (std::size_t i = 0; i < num_items; ++i) {
container.push_back(Item());
}
return 0;
}
Mit g ++ (GCC) 4.8.2 20131219 (Vorabversion) und C ++ 03 bekomme ich:
milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
35.206824 task-clock # 0.988 CPUs utilized ( +- 1.23% )
4 context-switches # 0.116 K/sec ( +- 4.38% )
0 cpu-migrations # 0.006 K/sec ( +- 66.67% )
849 page-faults # 0.024 M/sec ( +- 6.02% )
95,693,808 cycles # 2.718 GHz ( +- 1.14% ) [49.72%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
95,282,359 instructions # 1.00 insns per cycle ( +- 0.65% ) [75.27%]
30,104,021 branches # 855.062 M/sec ( +- 0.87% ) [77.46%]
6,038 branch-misses # 0.02% of all branches ( +- 25.73% ) [75.53%]
0.035648729 seconds time elapsed ( +- 1.22% )
Wenn C ++ 11 aktiviert ist, verschlechtert sich die Leistung erheblich:
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
86.485313 task-clock # 0.994 CPUs utilized ( +- 0.50% )
9 context-switches # 0.104 K/sec ( +- 1.66% )
2 cpu-migrations # 0.017 K/sec ( +- 26.76% )
798 page-faults # 0.009 M/sec ( +- 8.54% )
237,982,690 cycles # 2.752 GHz ( +- 0.41% ) [51.32%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
135,730,319 instructions # 0.57 insns per cycle ( +- 0.32% ) [75.77%]
30,880,156 branches # 357.057 M/sec ( +- 0.25% ) [75.76%]
4,188 branch-misses # 0.01% of all branches ( +- 7.59% ) [74.08%]
0.087016724 seconds time elapsed ( +- 0.50% )
Kann das jemand erklären? Bisher habe ich die Erfahrung gemacht, dass die STL durch die Aktivierung von C ++ 11 schneller wird, insb. dank Bewegungssemantik.
BEARBEITEN: Wie vorgeschlagen, wird container.emplace_back();
die Leistung stattdessen mit der C ++ 03-Version gleichgesetzt. Wie kann die C ++ 03-Version dasselbe erreichen push_back
?
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
36.229348 task-clock # 0.988 CPUs utilized ( +- 0.81% )
4 context-switches # 0.116 K/sec ( +- 3.17% )
1 cpu-migrations # 0.017 K/sec ( +- 36.85% )
798 page-faults # 0.022 M/sec ( +- 8.54% )
94,488,818 cycles # 2.608 GHz ( +- 1.11% ) [50.44%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
94,851,411 instructions # 1.00 insns per cycle ( +- 0.98% ) [75.22%]
30,468,562 branches # 840.991 M/sec ( +- 1.07% ) [76.71%]
2,723 branch-misses # 0.01% of all branches ( +- 9.84% ) [74.81%]
0.036678068 seconds time elapsed ( +- 0.80% )
push_back(Item())
umemplace_back()
in der C ++ 11 - Version?Antworten:
Ich kann Ihre Ergebnisse auf meinem Computer mit den Optionen reproduzieren, die Sie in Ihrem Beitrag geschrieben haben.
Wenn ich jedoch auch die Optimierung der Verbindungszeit aktiviere (ich übergebe das
-flto
Flag auch an gcc 4.7.2), sind die Ergebnisse identisch:(Ich kompiliere Ihren Originalcode mit
container.push_back(Item());
)Aus den Gründen muss man sich den generierten Assemblycode (
g++ -std=c++11 -O3 -S regr.cpp
) ansehen . Im C ++ 11-Modus ist der generierte Code wesentlich übersichtlicher als im C ++ 98-Modus, und das Inlining der Funktionvoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
schlägt im C ++ 11-Modus standardmäßig fehl
inline-limit
.Diese fehlgeschlagene Inline hat einen Dominoeffekt. Nicht weil diese Funktion aufgerufen wird (sie wird nicht einmal aufgerufen!), Sondern weil wir vorbereitet sein müssen: Wenn sie aufgerufen wird, müssen die Funktionsargumente (
Item.a
undItem.b
) bereits an der richtigen Stelle sein. Dies führt zu einem ziemlich chaotischen Code.Hier ist der relevante Teil des generierten Codes für den Fall, dass Inlining erfolgreich ist :
Dies ist eine schöne und kompakte for-Schleife. Vergleichen wir dies nun mit dem des fehlgeschlagenen Inline- Falls:
Dieser Code ist unübersichtlich und in der Schleife ist viel mehr los als im vorherigen Fall. Vor der Funktion
call
(letzte Zeile) müssen die Argumente entsprechend platziert werden:Obwohl dies nie ausgeführt wird, ordnet die Schleife die Dinge vorher an:
Dies führt zu dem unordentlichen Code. Wenn es keine Funktion gibt,
call
weil das Inlining erfolgreich ist, haben wir nur 2 Verschiebungsanweisungen in der Schleife und es gibt kein Durcheinander mit dem%rsp
(Stapelzeiger). Wenn das Inlining jedoch fehlschlägt, erhalten wir 6 Züge und wir spielen viel mit dem%rsp
.Nur um meine Theorie zu untermauern (beachten Sie die
-finline-limit
), beide im C ++ 11-Modus:Wenn wir den Compiler bitten, sich nur ein wenig mehr Mühe zu geben, um diese Funktion zu integrieren, verschwindet der Leistungsunterschied.
Was bringt diese Geschichte? Dass fehlgeschlagene Inlines viel kosten können und Sie die Compiler-Funktionen voll ausnutzen sollten: Ich kann nur die Optimierung der Verbindungszeit empfehlen. Es gab meinen Programmen einen signifikanten Leistungsschub (bis zu 2,5x) und alles was ich tun musste, war die
-flto
Flagge zu übergeben. Das ist ein ziemlich guter Deal! ;)Ich empfehle jedoch nicht, Ihren Code mit dem Inline-Schlüsselwort zu verwerfen. Lassen Sie den Compiler entscheiden, was zu tun ist. (Der Optimierer darf das Inline-Schlüsselwort ohnehin als Leerzeichen behandeln.)
Gute Frage, +1!
quelle
inline
hat nichts mit Funktionsinlining zu tun; es bedeutet "definiert inline" nicht "bitte inline dies". Wenn Sie tatsächlich nach Inlining fragen möchten, verwenden Sie__attribute__((always_inline))
oder ähnliches.inline
Dies ist auch eine Anfrage an den Compiler, dass die Funktion eingebunden werden soll, und beispielsweise, dass der Intel C ++ - Compiler Leistungswarnungen ausgibt, wenn er Ihre Anfrage nicht erfüllt. (Ich habe icc in letzter Zeit nicht überprüft, ob dies immer noch der Fall ist.) Leider habe ich gesehen, wie Leute ihren Code mit dem Papierkorb weggeworfen habeninline
und darauf gewartet haben, dass ein Wunder geschieht. Ich würde nicht verwenden__attribute__((always_inline))
; Wahrscheinlich wissen die Compiler-Entwickler besser, was sie inline und was nicht. (Trotz des Gegenbeispiels hier.)inline
Spezifizierer gibt der Implementierung an, dass die Inline-Substitution des Funktionskörpers am Aufrufpunkt dem üblichen Funktionsaufrufmechanismus vorzuziehen ist." (§7.1.2.2) Für diese Optimierung sind jedoch keine Implementierungen erforderlich, da es größtenteils ein Zufall ist, dassinline
Funktionen häufig gute Kandidaten für Inlining sind. Es ist also besser, explizit zu sein und ein Compiler-Pragma zu verwenden.