Ich sehe immer wieder Leute, die behaupten, dass die MOV-Anweisung in x86 wegen der Umbenennung von Registern kostenlos sein kann.
Ich kann das beim besten Willen nicht in einem einzigen Testfall überprüfen. Jeder Testfall, den ich versuche, entlarvt es.
Hier ist zum Beispiel der Code, den ich mit Visual C++ kompiliere:
#include <limits.h>
#include <stdio.h>
#include <time.h>
int main(void)
{
unsigned int k, l, j;
clock_t tstart = clock();
for (k = 0, j = 0, l = 0; j < UINT_MAX; ++j)
{
++k;
k = j; // <-- comment out this line to remove the MOV instruction
l += j;
}
fprintf(stderr, "%d ms\n", (int)((clock() - tstart) * 1000 / CLOCKS_PER_SEC));
fflush(stderr);
return (int)(k + j + l);
}
Dies erzeugt den folgenden Assembler-Code für die Schleife (Sie können dies nach Belieben erstellen; Sie benötigen Visual C++ offensichtlich nicht):
LOOP:
add edi,esi
mov ebx,esi
inc esi
cmp esi,FFFFFFFFh
jc LOOP
Jetzt führe ich dieses Programm mehrmals aus und beobachte einen ziemlich konstanten Unterschied von 2%, wenn die MOV-Anweisung entfernt wird:
Without MOV With MOV
1303 ms 1358 ms
1324 ms 1363 ms
1310 ms 1345 ms
1304 ms 1343 ms
1309 ms 1334 ms
1312 ms 1336 ms
1320 ms 1311 ms
1302 ms 1350 ms
1319 ms 1339 ms
1324 ms 1338 ms
Also was gibt? Warum ist MOV nicht “kostenlos”? Ist diese Schleife zu kompliziert für x86?
Gibt es eine Single Beispiel da draußen, das zeigen kann, dass MOV kostenlos ist, wie die Leute behaupten?
Wenn ja, was ist es? Und wenn nicht, warum behaupten immer alle, MOV sei kostenlos?
Bei der “Freiheit” geht es um Latenz, die Sie hier nicht messen. Auch 2% davon sind deutlich weniger als ein Zyklus, also nur aufgrund von “seltsamen Effekten”.
– Harald
24. Mai 2017 um 22:19 Uhr
Nun, was bedeutet “vollständig entfernt” überhaupt. Natürlich kann es vor dem Decodieren nicht entfernt werden, da noch nicht einmal bekannt ist, was es ist. Es überrascht nicht, dass der Umbenennungstrick den Mov bestenfalls während des Umbenennens entfernen kann und dann nicht einmal immer. Nur weil sie da ist, kann die Bewegung nicht sein völlig frei.
– Harald
24. Mai 2017 um 22:35 Uhr
Sie haben 25 % mehr Anweisungen hinzugefügt, aber es ist nur 2 % langsamer. Sie können das nicht mit “scheint es gibt keine MOV-Eliminierung” erklären. Ein Unterschied von 2 % erfordert eine andere Erklärung, wie z. B. dass der Kern zu heiß wird und zurück gedrosselt wird.
– Hans Passant
24. Mai 2017 um 22:59 Uhr
Durch die Registerumbenennung wird der MOV effektiv aus dem Back-End entfernt, was bedeutet, dass er aus 0 µops besteht, keinen Ausführungsport verbraucht und eine Latenz von 0 hat. Die Anweisung selbst muss jedoch noch entschlüsselt werden, was nicht kostenlos ist. Außerdem nimmt es Platz im Code ein, was Platz im Cache bedeutet. Also nein, ein MOV ist nie wirklich kostenlos, weil es im Frontend Kosten gibt, aber es ist oft so effektiv frei im Kontext eines größeren Codeblocks, der eine sinnvolle Operation ausführt. Ein Unterschied von 2% in der Ausführungsgeschwindigkeit ist deutlich weit weniger als ein Zyklus, wie man naiv erwarten würde.
– Cody Grey
♦
26. Mai 2017 um 9:44 Uhr
@CodyGray: Ein eliminiertes MOV nimmt Platz im ROB ein, bis es auf Intel-Hardware zurückgezogen wird (wie eine xor-Null-Anweisung oder sogar ein NOP) (ohne Fehlvorhersagen für Zweige,
uops_retired.retire_slots
wird fast genau passenuops_issued.any
). Mein mentales Modell ist, dass sie in den ROB (Fused-Domain) in einem bereits ausgeführten Zustand eintreten, in dem sie sich zurückziehen können, ohne dass Uops der unfusionierten Domain in den RS (Scheduler) ausgegeben werden. Vermutlich gibt es etwas nicht Triviales daran, keine uop zu haben, um sich für eine Anweisung zurückzuziehen, vielleicht etwas über die Aktualisierung von RIP oder einfach über das Zurücksetzen von Fehlspekulationen …– Peter Cordes
28. Mai 2017 um 3:59 Uhr