First of all, thanks for posting this question/challenge! As a disclaimer, I'm a native C programmer with some Fortran experience, and feel most at home in C, so as such, I will focus only on improving the C version. I invite all Fortran hacks to have their go too!
Just to remind newcomers about what this is about: The basic premise in this thread was that gcc/fortran and icc/ifort should, since they have the same back-ends respectively, produce equivalent code for the same (semantically identical) program, irrespective of it being in C or Fortran. The quality of the result depends only on the quality of the respective implementations.
I played around with the code a bit, and on my computer (ThinkPad 201x, Intel Core i5 M560, 2.67 GHz), using gcc
4.6.1 and the following compiler flags:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
I also went ahead and wrote a SIMD-vectorized C-language version of the C++ code, 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;
}
All three versions were compiled with the same flags and the same gcc
version. Note that I wrapped the main function call in a loop from 0..9 to get more accurate timings.
$ 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
So with "better" compiler flags, the C++ version out-performs the Fortran version and hand-coded vectorized loops only provide a marginal improvement. A quick look at the assembler for the C++ version shows that the main loops have also been vectorized, albeit unrolled more aggressively.
I also had a look at the assembler generated by gfortran
and here's the big surprise: no vectorization. I attribute the fact that it's only marginally slower to the problem being bandwidth limited, at least on my architecture. For each of the matrix multiplications, 230MB of data are traversed, which pretty-much swamps all levels of cache. If you use a smaller input value, e.g. 100
, the performance differences grow considerably.
As a side-note, instead of obsessing about vectorization, alignment and compiler flags, the most obvious optimization would be to compute the first few iterations in single-precision arithmetic, until we have ~8 digits of the result. The single-precision instructions are not only faster, but the amount of memory that has to be moved around is also halved.