Trước hết, cảm ơn bạn đã đăng câu hỏi / thử thách này! Từ chối trách nhiệm, tôi là một lập trình viên C bản địa có một số kinh nghiệm về Fortran và cảm thấy thoải mái nhất khi ở nhà C, vì vậy, tôi sẽ chỉ tập trung vào việc cải thiện phiên bản C. Tôi cũng mời tất cả các hack của Fortran đi!
Chỉ để nhắc nhở những người mới về những gì này là về: Những tiền đề cơ bản trong này chủ đề là gcc / fortran và icc / ifort nên, kể từ khi họ có cùng một back-đầu tương ứng, sản xuất mã tương đương cho cùng một chương trình (ngữ nghĩa giống hệt nhau), không phân biệt của nó ở C hoặc Fortran. Chất lượng của kết quả chỉ phụ thuộc vào chất lượng của việc thực hiện tương ứng.
Tôi đã chơi xung quanh với mã một chút và trên máy tính của tôi (ThinkPad 201x, Intel Core i5 M560, 2.67 GHz), sử dụng gcc
4.6.1 và các cờ trình biên dịch sau:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
Tôi cũng đã tiếp tục và viết một phiên bản ngôn ngữ C được mã hóa bằng SIMD của mã 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;
}
Tất cả ba phiên bản được biên dịch với cùng một cờ và cùng một gcc
phiên bản. Lưu ý rằng tôi đã gói lệnh gọi hàm chính trong một vòng lặp từ 0..9 để có được thời gian chính xác hơn.
$ 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
Vì vậy, với các cờ biên dịch "tốt hơn", phiên bản C ++ thực hiện tốt hơn phiên bản Fortran và các vòng lặp được mã hóa bằng tay chỉ cung cấp một cải tiến nhỏ. Nhìn nhanh vào trình biên dịch cho phiên bản C ++ cho thấy các vòng lặp chính cũng đã được vector hóa, mặc dù không được kiểm soát mạnh mẽ hơn.
Tôi cũng đã xem xét trình biên dịch được tạo bởi gfortran
và đây là một bất ngờ lớn: không có vector hóa. Tôi cho rằng thực tế là nó chỉ chậm hơn một chút đối với vấn đề bị giới hạn băng thông, ít nhất là trên kiến trúc của tôi. Đối với mỗi phép nhân ma trận, 230 MB dữ liệu được duyệt qua, điều này khá nhiều thay đổi tất cả các cấp độ của bộ đệm. Nếu bạn sử dụng giá trị đầu vào nhỏ hơn, vd100
sự khác biệt về hiệu suất sẽ tăng đáng kể.
Như một lưu ý phụ, thay vì ám ảnh về vector hóa, căn chỉnh và cờ trình biên dịch, tối ưu hóa rõ ràng nhất sẽ là tính toán một vài lần lặp đầu tiên trong số học chính xác đơn, cho đến khi chúng ta có ~ 8 chữ số của kết quả. Các hướng dẫn chính xác đơn không chỉ nhanh hơn mà lượng bộ nhớ phải di chuyển cũng giảm đi một nửa.