Antes de tudo, obrigado por postar esta pergunta / desafio! Como isenção de responsabilidade, sou programador nativo em C com alguma experiência em Fortran e me sinto mais à vontade em C; portanto, vou me concentrar apenas em melhorar a versão em C. Convido todos os hacks do Fortran a fazerem o mesmo!
Apenas para lembrar aos recém-chegados sobre o que é isso: A premissa básica neste segmento era que gcc / fortran e icc / ifort deveriam, uma vez que possuem os mesmos back-ends, respectivamente, produzir código equivalente para o mesmo programa (semanticamente idêntico), independentemente sendo em C ou Fortran. A qualidade do resultado depende apenas da qualidade das respectivas implementações.
Eu brinquei um pouco com o código e no meu computador (ThinkPad 201x, Intel Core i5 M560, 2,67 GHz), usando gcc4.6.1 e os seguintes sinalizadores do compilador:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Também fui adiante e escrevi uma versão em linguagem C do código C ++ vetorizada pelo SIMD 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;
}
Todas as três versões foram compiladas com os mesmos sinalizadores e os mesmos gcc versão. Observe que envolvi a chamada de função principal em um loop de 0 a 9 para obter tempos mais 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
Portanto, com sinalizadores "melhores" do compilador, a versão C ++ supera a versão Fortran e os loops vetorizados codificados à mão fornecem apenas uma melhoria marginal. Uma rápida olhada no assembler para a versão C ++ mostra que os loops principais também foram vetorizados, embora desenrolados de forma mais agressiva.
Também dei uma olhada no assembler gerado pelo gfortran e aqui está a grande surpresa: sem vetorização. Atribuo o fato de que é apenas um pouco mais lento ao problema de a largura de banda ser limitada, pelo menos na minha arquitetura. Para cada uma das multiplicações de matriz, são percorridos 230 MB de dados, o que praticamente inverte todos os níveis de cache. Se você usar um valor de entrada menor, por exemplo 100, as diferenças de desempenho aumentam consideravelmente.
Como observação, em vez de ficar obcecado com sinalizações de vetorização, alinhamento e compilador, a otimização mais óbvia seria calcular as primeiras iterações na aritmética de precisão única, até obtermos ~ 8 dígitos do resultado. As instruções de precisão única não são apenas mais rápidas, mas a quantidade de memória que precisa ser movida também é reduzida pela metade.