सबसे पहले, इस प्रश्न / चुनौती को पोस्ट करने के लिए धन्यवाद! अस्वीकरण के रूप में, मैं कुछ फोरट्रान अनुभव के साथ एक मूल सी प्रोग्रामर हूं, और सी में घर पर सबसे अधिक महसूस करता हूं, इसलिए, मैं केवल सी संस्करण को बेहतर बनाने पर ध्यान केंद्रित करूंगा। मैं सभी फोरट्रान हैक्स को अपने पास जाने के लिए आमंत्रित करता हूँ!
नए लोगों को यह याद दिलाने के बारे में कि यह किस बारे में है: इस धागे में मूल आधार यह था कि gcc / फोरट्रान और icc / ifort चाहिए, क्योंकि उनके पास क्रमशः एक ही बैक-एंड होते हैं, समान (शब्दार्थ समान) प्रोग्राम के लिए समान कोड का उत्पादन करते हैं, चाहे जो भी हो इसका C या फोरट्रान में होना। परिणाम की गुणवत्ता केवल संबंधित कार्यान्वयन की गुणवत्ता पर निर्भर करती है।
मैंने कोड के साथ थोड़ा सा खेला, और अपने कंप्यूटर पर (थिंकपैड 201x, Intel Core i5 M560, 2.67 GHz), gcc
4.6.1 और निम्न कंपाइलर झंडे का उपयोग किया:
GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing
मैंने भी आगे बढ़कर C ++ कोड का SIMD- वेक्टरकृत 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
तो "बेहतर" संकलक झंडे के साथ, सी ++ संस्करण आउट फोरट्रान संस्करण और हाथ से कोडित वेक्टर लूप केवल एक मामूली सुधार प्रदान करता है। सी ++ संस्करण के लिए कोडांतरक पर एक त्वरित नज़र से पता चलता है कि मुख्य छोरों को भी सदिश कर दिया गया है, यद्यपि बहुत आक्रामक तरीके से अनियंत्रित।
मुझे इस बात का भी आभास हो गया है कि gfortran
यहां पर मौजूद असेम्बलर और बड़े आश्चर्य की बात है: कोई वैश्वीकरण नहीं। मैं इस तथ्य को विशेषता देता हूं कि यह समस्या सीमित बैंडविड्थ तक सीमित है, कम से कम मेरी वास्तुकला पर। मैट्रिक्स के प्रत्येक गुणन के लिए, 230MB डेटा का पता लगाया जाता है, जो कैश के सभी स्तरों पर बहुत अधिक दलदल करता है। यदि आप एक छोटे इनपुट मान का उपयोग करते हैं, उदाहरण के लिए 100
, प्रदर्शन अंतर काफी बढ़ जाता है।
साइड-नोट के रूप में, वेक्टराइजेशन, संरेखण और संकलक झंडे के बारे में ध्यान देने के बजाय, सबसे स्पष्ट अनुकूलन एकल-सटीक अंकगणित में पहले कुछ पुनरावृत्तियों की गणना करना होगा, जब तक कि हमारे पास परिणाम के ~ 8 अंक न हों। एकल-सटीक निर्देश न केवल तेज़ हैं, बल्कि स्मृति की मात्रा जो चारों ओर ले जानी है, भी आधी है।