J'ai essayé de me faire une idée de l'impact d'avoir un tableau dans le cache L1 par rapport à la mémoire en chronométrant une routine qui met à l'échelle et additionne les éléments d'un tableau en utilisant le code suivant (je suis conscient que je devrais simplement mettre à l'échelle le résultat par 'a' à la fin ; le but est de faire à la fois une multiplication et une addition dans la boucle - jusqu'à présent, le compilateur n'a pas compris comment factoriser 'a') :
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;
}
Notez que les routines timer() et fill() ne sont pas incluses pour des raisons de brièveté ; leur source complète peut être trouvée ici si vous voulez exécuter le code :
Maintenant, c'est là que ça devient intéressant. Voici la sortie :
Operand size: 2048
Vector size 2048: mflops=588.8, result=-67.8
Il s'agit d'une performance totalement hors cache, malgré le fait que tous les éléments de X devraient être conservés en cache entre les itérations de la boucle. En regardant le code d'assemblage généré par :
g++ -O3 -S -fno-asynchronous-unwind-tables register_opt_example.cpp
Je remarque une bizarrerie dans la boucle de la fonction somme :
L55:
movsd (%r12,%rax,8), %xmm0
mulsd %xmm1, %xmm0
addsd -72(%rbp), %xmm0
movsd %xmm0, -72(%rbp)
incq %rax
cmpq $2048, %rax
jne L55
Les instructions :
addsd -72(%rbp), %xmm0
movsd %xmm0, -72(%rbp)
indiquent qu'il stocke la valeur de "total" dans sum() sur la pile, et la lit et l'écrit à chaque itération de la boucle. J'ai modifié l'assemblage pour que cet opérande soit conservé dans un registre :
...
addsd %xmm0, %xmm3
...
Ce petit changement crée un énorme une augmentation des performances :
Operand size: 2048
Vector size 2048: mflops=1958.9, result=-67.8
tl;dr Ma question est la suivante : pourquoi le remplacement d'un accès à un emplacement mémoire unique par un registre accélère-t-il autant le code, étant donné que cet emplacement unique devrait être stocké dans le cache L1 ? Quels sont les facteurs architecturaux qui rendent cela possible ? Il semble très étrange que l'écriture répétée d'un emplacement de pile détruise complètement l'efficacité d'un cache.
Annexe
Ma version de gcc est :
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)
Mon processeur est :
Intel Xeon X5650