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 gcc
4.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.