Прежде всего, спасибо за размещение этого вопроса / задачи! Как заявление об отказе от ответственности, я - нативный программист на Си с некоторым опытом работы на Фортране, и чувствую себя как дома в Си, поэтому я сосредоточусь только на улучшении версии Си. Я приглашаю всех хаков Фортрана, чтобы они тоже пошли!
Просто чтобы напомнить новичкам о том, что это такое: основная предпосылка в этом потоке заключалась в том, что gcc / fortran и icc / ifort должны, поскольку они имеют одинаковые серверные части соответственно, генерировать эквивалентный код для одной и той же (семантически идентичной) программы, независимо от того, это быть в C или Fortran. Качество результата зависит только от качества соответствующих реализаций.
Я немного поиграл с кодом и на своем компьютере (ThinkPad 201x, Intel Core i5 M560, 2,67 ГГц), используя gcc
4.6.1 и следующие флаги компилятора:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Я также пошел дальше и написал SIM-векторизованную версию C ++ на 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;
}
Все три версии были скомпилированы с одинаковыми флагами и одинаковой gcc
версией. Обратите внимание, что я обернул вызов основной функции в цикле с 0..9, чтобы получить более точные значения времени.
$ 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
Таким образом, с «лучшими» флагами компилятора версия C ++ превосходит версию Fortran, а векторизованные циклы с ручным кодированием обеспечивают лишь незначительное улучшение. Беглый взгляд на ассемблер для версии C ++ показывает, что основные циклы также были векторизованы, хотя и развернуты более агрессивно.
Я также взглянул на ассемблер, сгенерированный gfortran
и вот большой сюрприз: нет векторизации. Я связываю тот факт, что это только незначительно медленнее с проблемой ограничения пропускной способности, по крайней мере, в моей архитектуре. Для каждого умножения матрицы пройдено 230 МБ данных, что в значительной степени перекрывает все уровни кэша. Если вы используете меньшее входное значение, например100
, различия в производительности значительно возрастут.
В качестве дополнительного примечания, вместо того, чтобы зацикливаться на флагах векторизации, выравнивания и компилятора, наиболее очевидной оптимизацией будет вычисление первых нескольких итераций в арифметике с одинарной точностью, пока мы не получим ~ 8 цифр результата. Инструкции одинарной точности не только быстрее, но и объем памяти, который необходимо перемещать, также уменьшается вдвое.