En primer lugar, ¡gracias por publicar esta pregunta / desafío! Como descargo de responsabilidad, soy un programador nativo de C con algo de experiencia en Fortran, y me siento como en casa en C, por lo que me centraré solo en mejorar la versión C. ¡Invito a todos los piratas de Fortran a probar también!
Solo para recordarles a los recién llegados de qué se trata: la premisa básica en este hilo era que gcc / fortran e icc / ifort deberían, dado que tienen los mismos back-end respectivamente, producir código equivalente para el mismo programa (semánticamente idéntico), independientemente de que sea en C o Fortran. La calidad del resultado depende solo de la calidad de las implementaciones respectivas.
Jugué un poco con el código y en mi computadora (ThinkPad 201x, Intel Core i5 M560, 2.67 GHz), usando gcc
4.6.1 y los siguientes indicadores de compilación:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
También seguí adelante y escribí una versión SIMD-vectorizada en lenguaje C del código 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;
}
Las tres versiones fueron compiladas con las mismas banderas y la misma gcc
versión. Tenga en cuenta que envolví la llamada a la función principal en un bucle de 0..9 para obtener tiempos más precisos.
$ 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
Entonces, con indicadores de compilador "mejores", la versión de C ++ supera a la versión de Fortran y los bucles vectorizados codificados a mano solo proporcionan una mejora marginal. Un vistazo rápido al ensamblador para la versión C ++ muestra que los bucles principales también se han vectorizado, aunque se han desenrollado de forma más agresiva.
También eché un vistazo al ensamblador generado por gfortran
y aquí está la gran sorpresa: sin vectorización. Atribuyo el hecho de que es solo un poco más lento al problema de que el ancho de banda es limitado, al menos en mi arquitectura. Para cada una de las multiplicaciones de la matriz, se atraviesan 230 MB de datos, lo que prácticamente inunda todos los niveles de caché. Si utiliza un valor de entrada más pequeño, por ejemplo 100
, las diferencias de rendimiento aumentan considerablemente.
Como nota al margen, en lugar de obsesionarse con las banderas de vectorización, alineación y compilación, la optimización más obvia sería calcular las primeras iteraciones en aritmética de precisión simple, hasta que tengamos ~ 8 dígitos del resultado. Las instrucciones de precisión simple no solo son más rápidas, sino que la cantidad de memoria que debe moverse también se reduce a la mitad.