Tout d'abord, merci d'avoir posté cette question / défi! Comme avertissement, je suis un programmeur C natif avec une certaine expérience de Fortran, et je me sens plus à l'aise en C, donc en tant que tel, je me concentrerai uniquement sur l'amélioration de la version C. J'invite tous les hacks de Fortran à essayer aussi!
Juste pour rappeler aux nouveaux arrivants de quoi il s'agit: la prémisse de base dans ce fil était que gcc / fortran et icc / ifort devraient, puisqu'ils ont respectivement les mêmes back-ends, produire un code équivalent pour le même programme (sémantiquement identique), indépendamment d'être en C ou Fortran. La qualité du résultat ne dépend que de la qualité des implémentations respectives.
J'ai joué un peu avec le code et sur mon ordinateur (ThinkPad 201x, Intel Core i5 M560, 2,67 GHz), en utilisant gcc
4.6.1 et les drapeaux de compilation suivants:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Je suis aussi allé de l' avant et écrit un SIMD vectorisé version langage C du code C ++, spectral_norm_vec.c
:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
/* Define the generic vector type macro. */
#define vector(elcount, type) __attribute__((vector_size((elcount)*sizeof(type)))) type
double Ac(int i, int j)
{
return 1.0 / ((i+j) * (i+j+1)/2 + i+1);
}
double dot_product2(int n, double u[], double v[])
{
double w;
int i;
union {
vector(2,double) v;
double d[2];
} *vu = u, *vv = v, acc[2];
/* Init some stuff. */
acc[0].d[0] = 0.0; acc[0].d[1] = 0.0;
acc[1].d[0] = 0.0; acc[1].d[1] = 0.0;
/* Take in chunks of two by two doubles. */
for ( i = 0 ; i < (n/2 & ~1) ; i += 2 ) {
acc[0].v += vu[i].v * vv[i].v;
acc[1].v += vu[i+1].v * vv[i+1].v;
}
w = acc[0].d[0] + acc[0].d[1] + acc[1].d[0] + acc[1].d[1];
/* Catch leftovers (if any) */
for ( i = n & ~3 ; i < n ; i++ )
w += u[i] * v[i];
return w;
}
void matmul2(int n, double v[], double A[], double u[])
{
int i, j;
union {
vector(2,double) v;
double d[2];
} *vu = u, *vA, vi;
bzero( u , sizeof(double) * n );
for (i = 0; i < n; i++) {
vi.d[0] = v[i];
vi.d[1] = v[i];
vA = &A[i*n];
for ( j = 0 ; j < (n/2 & ~1) ; j += 2 ) {
vu[j].v += vA[j].v * vi.v;
vu[j+1].v += vA[j+1].v * vi.v;
}
for ( j = n & ~3 ; j < n ; j++ )
u[j] += A[i*n+j] * v[i];
}
}
void matmul3(int n, double A[], double v[], double u[])
{
int i;
for (i = 0; i < n; i++)
u[i] = dot_product2( n , &A[i*n] , v );
}
void AvA(int n, double A[], double v[], double u[])
{
double tmp[n] __attribute__ ((aligned (16)));
matmul3(n, A, v, tmp);
matmul2(n, tmp, A, u);
}
double spectral_game(int n)
{
double *A;
double u[n] __attribute__ ((aligned (16)));
double v[n] __attribute__ ((aligned (16)));
int i, j;
/* Aligned allocation. */
/* A = (double *)malloc(n*n*sizeof(double)); */
if ( posix_memalign( (void **)&A , 4*sizeof(double) , sizeof(double) * n * n ) != 0 ) {
printf( "spectral_game:%i: call to posix_memalign failed.\n" , __LINE__ );
abort();
}
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
A[i*n+j] = Ac(i, j);
}
}
for (i = 0; i < n; i++) {
u[i] = 1.0;
}
for (i = 0; i < 10; i++) {
AvA(n, A, u, v);
AvA(n, A, v, u);
}
free(A);
return sqrt(dot_product2(n, u, v) / dot_product2(n, v, v));
}
int main(int argc, char *argv[]) {
int i, N = ((argc >= 2) ? atoi(argv[1]) : 2000);
for ( i = 0 ; i < 10 ; i++ )
printf("%.9f\n", spectral_game(N));
return 0;
}
Les trois versions ont été compilées avec les mêmes drapeaux et la même gcc
version. Notez que j'ai enveloppé l'appel de fonction principale dans une boucle de 0 à 9 pour obtenir des timings plus précis.
$ time ./spectral_norm6 5500
1.274224153
...
real 0m22.682s
user 0m21.113s
sys 0m1.500s
$ time ./spectral_norm7 5500
1.274224153
...
real 0m21.596s
user 0m20.373s
sys 0m1.132s
$ time ./spectral_norm_vec 5500
1.274224153
...
real 0m21.336s
user 0m19.821s
sys 0m1.444s
Ainsi, avec de «meilleurs» indicateurs de compilateur, la version C ++ surpasse la version Fortran et les boucles vectorisées codées à la main ne fournissent qu'une amélioration marginale. Un rapide coup d'œil à l'assembleur de la version C ++ montre que les boucles principales ont également été vectorisées, bien que déroulées de manière plus agressive.
J'ai également regardé l'assembleur généré par gfortran
et voici la grande surprise: pas de vectorisation. J'attribue le fait qu'il n'est que légèrement plus lent au problème de la limitation de la bande passante, du moins sur mon architecture. Pour chacune des multiplications matricielles, 230 Mo de données sont parcourus, ce qui submerge à peu près tous les niveaux de cache. Si vous utilisez une valeur d'entrée plus petite, par exemple100
, les différences de performances augmentent considérablement.
En guise de remarque, au lieu d'obséder par la vectorisation, l'alignement et les drapeaux du compilateur, l'optimisation la plus évidente serait de calculer les premières itérations en arithmétique simple précision, jusqu'à ce que nous ayons ~ 8 chiffres du résultat. Les instructions en simple précision sont non seulement plus rapides, mais la quantité de mémoire à déplacer est également divisée par deux.