Prima di tutto, grazie per aver pubblicato questa domanda / sfida! Come disclaimer, sono un programmatore C nativo con qualche esperienza Fortran e mi sento quasi a casa in C, quindi come tale, mi concentrerò solo sul miglioramento della versione C. Invito anche tutti gli hack di Fortran a provare!
Giusto per ricordare ai nuovi arrivati di cosa si tratta: la premessa di base in questo thread era che gcc / fortran e icc / ifort dovrebbero, dato che hanno rispettivamente gli stessi back-end, produrre codice equivalente per lo stesso programma (semanticamente identico), indipendentemente di esso in C o Fortran. La qualità del risultato dipende solo dalla qualità delle rispettive implementazioni.
Ho giocato un po 'con il codice e sul mio computer (ThinkPad 201x, Intel Core i5 M560, 2,67 GHz), usando gcc
4.6.1 e le seguenti bandiere del compilatore:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Sono anche andato avanti e ho scritto una versione del codice C ++ in linguaggio C SIMD vettoriale 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;
}
Tutte e tre le versioni sono state compilate con gli stessi flag e la stessa gcc
versione. Si noti che ho racchiuso la chiamata della funzione principale in un ciclo da 0 a 9 per ottenere tempi più precisi.
$ 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
Quindi, con flag di compilatore "migliori", la versione C ++ supera la versione Fortran e i loop vettorizzati codificati a mano forniscono solo un miglioramento marginale. Una rapida occhiata all'assemblatore per la versione C ++ mostra che anche i loop principali sono stati vettorializzati, anche se srotolati in modo più aggressivo.
Ho anche dato un'occhiata all'assemblatore generato da gfortran
ed ecco la grande sorpresa: nessuna vettorializzazione. Attribuisco il fatto che è solo leggermente più lento al problema che la larghezza di banda è limitata, almeno sulla mia architettura. Per ciascuna delle moltiplicazioni di matrice, vengono attraversati 230 MB di dati, che praticamente sommergono tutti i livelli di cache. Se si utilizza un valore di input inferiore, ad esempio 100
, le differenze di prestazioni aumentano considerevolmente.
Come nota a margine, invece di essere ossessionato dalla flag di vettorializzazione, allineamento e compilatore, l'ottimizzazione più ovvia sarebbe quella di calcolare le prime iterazioni in aritmetica a precisione singola, fino a quando non avremo ~ 8 cifre del risultato. Le istruzioni a precisione singola non sono solo più veloci, ma anche la quantità di memoria che deve essere spostata viene dimezzata.