Ich habe gegenüber einem Kollegen behauptet, dass er if (i < input.size() - 1) print(0);
in dieser Schleife optimiert würde, damit er input.size()
nicht in jeder Iteration gelesen wird, aber es stellt sich heraus, dass dies nicht der Fall ist!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
Laut dem Compiler Explorer mit gcc-Optionen -O3 -fno-exceptions
lesen wir tatsächlich input.size()
jede Iteration und verwenden sie lea
, um eine Subtraktion durchzuführen!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
Interessanterweise findet diese Optimierung in Rust statt. Es sieht so aus, als würde es i
durch eine Variable ersetzt j
, die bei jeder Iteration dekrementiert wird, und der Test i < input.size() - 1
wird durch so etwas ersetzt j > 0
.
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
Im Compiler-Explorer sieht die entsprechende Assembly wie folgt aus:
cmpq %r12, %rbx
jae .LBB0_4
Ich habe nachgesehen und bin mir ziemlich sicher, dass es der Zähler r12
ist xs.len() - 1
und rbx
ist. Früher gibt es ein add
für rbx
und ein mov
außerhalb der Schleife in r12
.
Warum ist das? Es scheint, als ob GCC in der Lage ist, das zu integrieren, size()
und operator[]
wie es getan hat, sollte es wissen können, dass sich size()
dies nicht ändert. Aber vielleicht urteilt der Optimierer von GCC, dass es sich nicht lohnt, ihn in eine Variable zu ziehen? Oder vielleicht gibt es eine andere mögliche Nebenwirkung, die dies unsicher machen würde - weiß jemand Bescheid?
println
ist wahrscheinlich eine komplexe Methode, der Compiler kann Probleme haben zu beweisen, dassprintln
der Vektor nicht mutiert.cout.operator<<()
. Der Compiler weiß nicht, dass diese Black-Box-Funktion keinen Verweis auf diestd::vector
von einem globalen erhält .println
oder deroperator<<
Schlüssel ist.Antworten:
Der Nicht-Inline-Funktionsaufruf an
cout.operator<<(int)
ist eine Blackbox für den Optimierer (da die Bibliothek nur in C ++ geschrieben ist und der Optimierer nur einen Prototyp sieht; siehe Diskussion in den Kommentaren). Es muss davon ausgegangen werden, dass der Speicher, auf den möglicherweise eine globale Variable verweist, geändert wurde.(Oder der
std::endl
Anruf. Übrigens, warum an diesem Punkt eine Spülung erzwingen, anstatt nur eine zu drucken'\n'
?)zB ist nach allem, was es weiß,
std::vector<int> &input
ein Verweis auf eine globale Variable, und einer dieser Funktionsaufrufe modifiziert diese globale Variable . (Oder es gibtvector<int> *ptr
irgendwo eine globale Funktion oder eine Funktion, die einen Zeiger auf einestatic vector<int>
in einer anderen Kompilierungseinheit zurückgibt , oder auf eine andere Weise, mit der eine Funktion einen Verweis auf diesen Vektor erhalten kann, ohne dass wir einen Verweis darauf erhalten.Wenn Sie eine lokale Variable , deren Adresse hatte noch nie gemacht worden, der Compiler kann davon ausgehen , dass nicht-Inline - Funktion Anrufe nicht mutieren könnte. Weil es für keine globale Variable eine Möglichkeit gibt, einen Zeiger auf dieses Objekt zu halten. ( Dies wird als Escape-Analyse bezeichnet. ) Aus diesem Grund kann der Compiler
size_t i
über Funktionsaufrufe hinweg ein Register führen. (int i
Kann einfach wegoptimiert werden, weil es von beschattet wirdsize_t i
und nicht anderweitig verwendet wird).vector
Dies könnte auch mit einem lokalen Zeiger geschehen (dh mit den Zeigern base, end_size und end_capacity).ISO C99 hat eine Lösung für dieses Problem :
int *restrict foo
. Viele C ++ - Kompilierungen unterstützenint *__restrict foo
das Versprechen, dass auf den Speicher, auf den verwiesenfoo
wird, nur über diesen Zeiger zugegriffen wird. Am nützlichsten in Funktionen, die 2 Arrays benötigen, und Sie möchten dem Compiler versprechen, dass sie sich nicht überlappen. Es kann also automatisch vektorisiert werden, ohne Code zu generieren, um dies zu überprüfen und eine Fallback-Schleife auszuführen.Das OP kommentiert:
Das erklärt, warum Rust diese Optimierung durchführen kann, C ++ jedoch nicht.
Optimieren Sie Ihr C ++
Natürlich sollten Sie
auto size = input.size();
einmal oben in Ihrer Funktion verwenden, damit der Compiler weiß, dass es sich um eine Schleifeninvariante handelt. C ++ - Implementierungen lösen dieses Problem nicht für Sie, daher müssen Sie es selbst tun.Möglicherweise müssen Sie auch
const int *data = input.data();
Lasten des Datenzeigers vomstd::vector<int>
"Steuerblock" abheben. Es ist bedauerlich, dass für die Optimierung sehr nicht-idiomatische Quellenänderungen erforderlich sein können.Rust ist eine viel modernere Sprache, die entwickelt wurde, nachdem Compiler-Entwickler gelernt hatten, was in der Praxis für Compiler möglich war. Es zeigt sich auch auf andere Weise, einschließlich der portablen Darstellung einiger der coolen Dinge, die CPUs über
i32.count_ones
, Rotation, Bit-Scan usw. tun können . Es ist wirklich dumm, dass ISO C ++ immer noch keine dieser tragbaren Dinge verfügbar macht, außerstd::bitset::count()
.quelle
operator<<
für diese Operandentypen. In Standard C ++ ist es also keine Black Box und der Compiler kann davon ausgehen, dass er das tut, was in der Dokumentation angegeben ist. Vielleicht möchten sie Bibliotheksentwickler dabei unterstützen, nicht standardmäßiges Verhalten hinzuzufügen ...cout
Ermöglichtstreambuf
die Zuordnung eines Objekts einer benutzerdefinierten Klasse, von der abgeleitet wurde , zum Streamcout.rdbuf
. Ebenso kann ein von abgeleitetes Objektostream
zugeordnet werdencout.tie
.this
Zeiger implizit übergeben wird. Dies könnte in der Praxis bereits beim Konstruktor geschehen. Betrachten Sie diese einfache Schleife - ich habe nur die gcc-Hauptschleife (vonL34:
bisjne L34
) überprüft , aber sie verhält sich definitiv so, als wären die Vektorelemente entkommen (sie werden bei jeder Iteration aus dem Speicher geladen).