Pertama-tama, terima kasih telah mengirimkan pertanyaan / tantangan ini! Sebagai penafian, saya seorang programmer C asli dengan beberapa pengalaman Fortran, dan merasa paling nyaman di C, jadi dengan demikian, saya hanya akan fokus pada peningkatan versi C. Saya mengundang semua peretas Fortran untuk ikut serta!
Hanya untuk mengingatkan pendatang baru tentang apa ini: Premis dasar di utas ini adalah bahwa gcc / fortran dan icc / ifort harus, karena mereka masing-masing memiliki back-end yang sama, menghasilkan kode yang setara untuk program yang sama (identik secara semantik), terlepas dari itu berada di C atau Fortran. Kualitas hasil hanya tergantung pada kualitas implementasi masing-masing.
Saya bermain-main dengan kode sedikit, dan di komputer saya (ThinkPad 201x, Intel Core i5 M560, 2,67 GHz), menggunakan gcc
4.6.1 dan flag kompiler berikut:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Saya juga pergi ke depan dan menulis versi C-bahasa SIMD-Vectorized dari 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;
}
Ketiga versi dikompilasi dengan bendera yang sama dan gcc
versi yang sama . Perhatikan bahwa saya membungkus panggilan fungsi utama dalam satu lingkaran dari 0..9 untuk mendapatkan pengaturan waktu yang lebih akurat.
$ 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
Jadi dengan flag kompiler "lebih baik", versi C ++ mengalahkan versi Fortran dan loop vektorisasi kode tangan hanya memberikan peningkatan marjinal. Pandangan cepat pada assembler untuk versi C ++ menunjukkan bahwa loop utama juga telah di-vektor-kan, meskipun tidak dikontrol lebih agresif.
Saya juga melihat assembler yang dihasilkan oleh gfortran
dan inilah kejutan besar: tidak ada vektorisasi. Saya mengaitkan fakta bahwa hanya sedikit lebih lambat karena masalah terbatasnya bandwidth, setidaknya pada arsitektur saya. Untuk setiap perkalian matriks, data 230MB dilalui, yang cukup banyak menukar semua level cache. Jika Anda menggunakan nilai input yang lebih kecil, misalnya 100
, perbedaan kinerja tumbuh secara signifikan.
Sebagai catatan tambahan, alih-alih terobsesi dengan vektorisasi, penjajaran, dan flag kompiler, optimasi yang paling jelas adalah menghitung beberapa iterasi pertama dalam aritmatika presisi tunggal, sampai kita memiliki ~ 8 digit hasilnya. Instruksi presisi tunggal tidak hanya lebih cepat, tetapi jumlah memori yang harus dipindahkan juga berkurang setengahnya.