2014-05-09 8 views
6

ich viel vektorisierten Schleifen schreiben, so ein gemeinsames Idiom istWarum for Schleife hat 1 zusätzlichen Befehl als erwartet?

volatile int dummy[1<<10]; 
for (int64_t i = 0; i + 16 <= argc; i+= 16) // process all elements with whole vector 
{ 
    int x = dummy[i]; 
} 
// handle remainder (hopefully with SIMD too) 

Aber die resultierende Maschinencode hat 1 weitere Anweisung als Ich mag würde

.L3: 
     leaq -16(%rax), %rdx 
     addq $16, %rax 
     cmpq %rcx, %rax 
     movl -120(%rsp,%rdx,4), %edx 
     jbe  .L3 

(gcc 4.9 verwenden) Wenn ich ändern der Code for (int64_t i = 0; i <= argc - 16; i+= 16), dann ist die "extra" Anweisung ist weg:

.L2: 
     movl -120(%rsp,%rax,4), %ecx 
     addq $16, %rax 
     cmpq %rdx, %rax 
     jbe  .L2 

Aber warum der Unterschied? Ich dachte, vielleicht lag es an Schleifeninvarianten, aber zu vage. Dann bemerkte ich, dass im Instruktionsfall die Inkrementierung vor dem Laden erfolgt, was aufgrund der zweizeiligen Operandenanweisungen von x86 einen zusätzlichen mov erfordern würde. Eine andere Erklärung könnte also sein, dass es die Handelsanweisungs-Parallelität für 1 zusätzliche Anweisung ist.

Obwohl es scheint, dass es kaum Leistungsunterschiede geben würde, kann jemand dieses Geheimnis erklären (am besten, wer kennt Compiler-Transformationen)?

Im Idealfall würde Ich mag die ich halten + 16 < = Größe Form da, dass eine intuitive Bedeutung (das letzte Element des Vektors nicht außerhalb der Grenzen geht) hat

+1

Nur Fragen über optimierten Code stellen, nicht optimierter Code ist wörtlich für das Programm und nicht bezeichnend für die Geschwindigkeit überhaupt. Von dem, was Sie jetzt haben, wenn Sie den Optimierer einschalten, wird nichts mehr übrig bleiben. Natürlich können Sie das nicht schneller machen. –

+0

aber das ist optimierter Code (-O3), abgesehen von der flüchtigen Last? und es war, das Problem auf einfache Art zu illustrieren –

+0

Ich stimme zu supercat's Antwort ist sehr präzise, ​​aber es wird angenommen, dass ich transformiere i + 16 <= argc zu i <= argc - 16 ist notwendig, um die minimalen # Anweisungen zu erreichen. Ich würde gerne denken, wenn das notwendig ist. –

Antwort

8

Wenn argc unter -2147483632 waren und i war unter 2147483632, die Ausdrücke i+16 <= argc wären erforderlich, um ein arithmetisch korrektes Ergebnis zu erhalten, während der Ausdruck und i<argc-16 nicht. Die Notwendigkeit, in diesem Eckfall ein arithmetisch korrektes Ergebnis zu geben, hindert den Compiler daran, den früheren Ausdruck zu optimieren, um ihn dem letzteren anzupassen.

+0

Was für eine schreckliche Gotcha (wer würde eine negative Array-Größe verwenden)! Ich habe versucht, sowohl den Typ von argc als auch i zu ändern, um uint32_t zu sein, und die resultierende Instruktionszählung ist immer noch 5 wie erwartet, da ein Unterlauf immer noch auftreten kann. Aber wenn ich die Typen in int64 und uint64 ändere, reduziert sich die Instruktionszählung auf 4. Das wäre unerwartet, da ein Überlauf immer noch passieren kann, vorausgesetzt, der Compiler ist schon so streng? –

+0

Aber selbst wenn der Compiler i + 16 <= argc zu i <= argc - 16 nicht optimiert hat, sollte es in der Lage sein, 4 Anweisungen zu erzeugen. Aber das wird wahrscheinlich eine andere Frage sein, da meine Frage impliziert, warum i + 16 <= argc nicht dasselbe ist wie i <= argc - 16 –

+0

@YaleZhang: Wenn der Typ uint32 ist, dann ist i + 16 <= argc wird wahr wenn erwartet, oder wenn ich> 4294967280 oder argc <16. – supercat