Ich habe versucht, eine Vorstellung von den Auswirkungen eines Arrays im L1-Cache im Vergleich zum Speicher zu bekommen, indem ich eine Routine zeitlich festgelegt habe, die die Elemente eines Arrays mithilfe des folgenden Codes skaliert und summiert (mir ist bewusst, dass ich das Ergebnis nur mit 'skalieren sollte). a 'am Ende; der Punkt ist, sowohl ein Multiplizieren als auch ein Addieren innerhalb der Schleife durchzuführen - bisher hat der Compiler nicht herausgefunden, um' a 'herauszufiltern):
double sum(double a,double* X,int size)
{
double total = 0.0;
for(int i = 0; i < size; ++i)
{
total += a*X[i];
}
return total;
}
#define KB 1024
int main()
{
//Approximately half the L1 cache size of my machine
int operand_size = (32*KB)/(sizeof(double)*2);
printf("Operand size: %d\n", operand_size);
double* X = new double[operand_size];
fill(X,operand_size);
double seconds = timer();
double result;
int n_iterations = 100000;
for(int i = 0; i < n_iterations; ++i)
{
result = sum(3.5,X,operand_size);
//result += rand();
}
seconds = timer() - seconds;
double mflops = 2e-6*double(n_iterations*operand_size)/seconds;
printf("Vector size %d: mflops=%.1f, result=%.1f\n",operand_size,mflops,result);
return 0;
}
Beachten Sie, dass die Routinen timer () und fill () der Kürze halber nicht enthalten sind. Die vollständige Quelle finden Sie hier, wenn Sie den Code ausführen möchten:
Hier wird es interessant. Dies ist die Ausgabe:
Operand size: 2048
Vector size 2048: mflops=588.8, result=-67.8
Dies ist eine völlig nicht zwischengespeicherte Leistung, obwohl alle Elemente von X zwischen Schleifeniterationen im Cache gehalten werden sollten. Betrachten Sie den Assembler-Code, der generiert wurde von:
g++ -O3 -S -fno-asynchronous-unwind-tables register_opt_example.cpp
Ich bemerke eine Kuriosität in der Summenfunktionsschleife:
L55:
movsd (%r12,%rax,8), %xmm0
mulsd %xmm1, %xmm0
addsd -72(%rbp), %xmm0
movsd %xmm0, -72(%rbp)
incq %rax
cmpq $2048, %rax
jne L55
Die Anleitungen:
addsd -72(%rbp), %xmm0
movsd %xmm0, -72(%rbp)
Geben Sie an, dass der Wert von "total" in sum () auf dem Stapel gespeichert und bei jeder Schleifeniteration gelesen und geschrieben wird. Ich habe die Assembly so geändert, dass dieser Operand in einem Register gespeichert wird:
...
addsd %xmm0, %xmm3
...
Diese kleine Änderung sorgt für einen enormen Leistungsschub:
Operand size: 2048
Vector size 2048: mflops=1958.9, result=-67.8
tl; dr Meine Frage lautet: Warum beschleunigt das Ersetzen eines Zugriffs auf einen einzelnen Speicherort durch ein Register den Code so stark, da der einzelne Speicherort im L1-Cache gespeichert werden sollte? Welche architektonischen Faktoren machen dies möglich? Es scheint sehr seltsam, dass das wiederholte Schreiben eines Stack-Speicherorts die Effektivität eines Caches vollständig zerstören würde.
Blinddarm
Meine gcc-Version ist:
Target: i686-apple-darwin10
Configured with: /var/tmp/gcc/gcc-5646.1~2/src/configure --disable-checking --enable-werror --prefix=/usr --mandir=/share/man --enable-languages=c,objc,c++,obj-c++ --program-transform-name=/^[cg][^.-]*$/s/$/-4.2/ --with-slibdir=/usr/lib --build=i686-apple-darwin10 --with-gxx-include-dir=/include/c++/4.2.1 --program-prefix=i686-apple-darwin10- --host=x86_64-apple-darwin10 --target=i686-apple-darwin10
Thread model: posix
gcc version 4.2.1 (Apple Inc. build 5646) (dot 1)
Meine CPU ist:
Intel Xeon X5650