Zunächst einmal vielen Dank für das Posten dieser Frage / Herausforderung! Als Haftungsausschluss bin ich ein gebürtiger C-Programmierer mit Fortran-Erfahrung und fühle mich in C am wohlsten. Daher werde ich mich nur auf die Verbesserung der C-Version konzentrieren. Ich lade alle Fortran-Hacks ein, mitzumachen!
Nur um Neulinge daran zu erinnern, worum es geht: Die Grundvoraussetzung in diesem Thread war, dass gcc / fortran und icc / ifort, da sie jeweils die gleichen Backends haben, für dasselbe (semantisch identische) Programm einen entsprechenden Code erzeugen sollten, unabhängig davon davon in C oder Fortran. Die Qualität des Ergebnisses hängt nur von der Qualität der jeweiligen Implementierungen ab.
Ich habe ein wenig mit dem Code herumgespielt und auf meinem Computer (ThinkPad 201x, Intel Core i5 M560, 2,67 GHz) mit gcc
4.6.1 und den folgenden Compiler-Flags gearbeitet:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Ich habe auch eine SIMD-vektorisierte C-Sprachversion des C ++ - Codes geschrieben 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;
}
Alle drei Versionen wurden mit denselben Flags und derselben gcc
Version kompiliert . Beachten Sie, dass ich den Hauptfunktionsaufruf von 0..9 in eine Schleife eingebunden habe, um genauere Timings zu erhalten.
$ 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
Mit "besseren" Compiler-Flags übertrifft die C ++ - Version die Fortran-Version, und handcodierte vektorisierte Schleifen bieten nur eine marginale Verbesserung. Ein kurzer Blick auf den Assembler für die C ++ - Version zeigt, dass die Hauptschleifen ebenfalls vektorisiert wurden, wenn auch aggressiver abgewickelt.
Ich habe mir auch den Assembler von angeschaut gfortran
und hier ist die große Überraschung: keine Vektorisierung. Ich schreibe die Tatsache zu, dass es nur unwesentlich langsamer ist, wenn die Bandbreite begrenzt ist, zumindest in meiner Architektur. Für jede der Matrixmultiplikationen werden 230 MB Daten durchlaufen, wodurch praktisch alle Cache-Ebenen überlastet werden. Wenn Sie beispielsweise einen kleineren Eingabewert verwenden 100
, nehmen die Leistungsunterschiede erheblich zu.
Anstatt von Vektorisierung, Ausrichtung und Compiler-Flags besessen zu sein, besteht die offensichtlichste Optimierung darin, die ersten paar Iterationen in Arithmetik mit einfacher Genauigkeit zu berechnen, bis wir ~ 8 Ziffern des Ergebnisses haben. Die Befehle mit einfacher Genauigkeit sind nicht nur schneller, sondern die Menge an Speicher, die verschoben werden muss, halbiert sich auch.