【发布时间】:2014-09-30 12:44:32
【问题描述】:
两个数组的点积
for(int i=0; i<n; i++) {
sum += x[i]*y[i];
}
不重用数据,所以它应该是一个内存绑定操作。因此,我应该能够从点积测量内存带宽。
使用代码 why-vectorizing-the-loop-does-not-have-performance-improvement 我的系统带宽为 9.3 GB/s。但是,当我尝试使用点积计算带宽时,我得到了单线程速率的两倍多和多线程速率的三倍多(我的系统有四个内核/八个超线程)。这对我来说毫无意义,因为内存绑定操作不应该从多个线程中受益。以下是以下代码的输出:
Xeon E5-1620, GCC 4.9.0, Linux kernel 3.13
dot 1 thread: 1.0 GB, sum 191054.81, time 4.98 s, 21.56 GB/s, 5.39 GFLOPS
dot_avx 1 thread 1.0 GB, sum 191043.33, time 5.16 s, 20.79 GB/s, 5.20 GFLOPS
dot_avx 2 threads: 1.0 GB, sum 191045.34, time 3.44 s, 31.24 GB/s, 7.81 GFLOPS
dot_avx 8 threads: 1.0 GB, sum 191043.34, time 3.26 s, 32.91 GB/s, 8.23 GFLOPS
谁能向我解释为什么我使用一个线程获得超过两倍的带宽,而使用多个线程获得超过三倍的带宽?
这是我使用的代码:
//g++ -O3 -fopenmp -mavx -ffast-math dot.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <x86intrin.h>
#include <omp.h>
extern "C" inline float horizontal_add(__m256 a) {
__m256 t1 = _mm256_hadd_ps(a,a);
__m256 t2 = _mm256_hadd_ps(t1,t1);
__m128 t3 = _mm256_extractf128_ps(t2,1);
__m128 t4 = _mm_add_ss(_mm256_castps256_ps128(t2),t3);
return _mm_cvtss_f32(t4);
}
extern "C" float dot_avx(float * __restrict x, float * __restrict y, const int n) {
x = (float*)__builtin_assume_aligned (x, 32);
y = (float*)__builtin_assume_aligned (y, 32);
float sum = 0;
#pragma omp parallel reduction(+:sum)
{
__m256 sum1 = _mm256_setzero_ps();
__m256 sum2 = _mm256_setzero_ps();
__m256 sum3 = _mm256_setzero_ps();
__m256 sum4 = _mm256_setzero_ps();
__m256 x8, y8;
#pragma omp for
for(int i=0; i<n; i+=32) {
x8 = _mm256_loadu_ps(&x[i]);
y8 = _mm256_loadu_ps(&y[i]);
sum1 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum1);
x8 = _mm256_loadu_ps(&x[i+8]);
y8 = _mm256_loadu_ps(&y[i+8]);
sum2 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum2);
x8 = _mm256_loadu_ps(&x[i+16]);
y8 = _mm256_loadu_ps(&y[i+16]);
sum3 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum3);
x8 = _mm256_loadu_ps(&x[i+24]);
y8 = _mm256_loadu_ps(&y[i+24]);
sum4 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum4);
}
sum += horizontal_add(_mm256_add_ps(_mm256_add_ps(sum1,sum2),_mm256_add_ps(sum3,sum4)));
}
return sum;
}
extern "C" float dot(float * __restrict x, float * __restrict y, const int n) {
x = (float*)__builtin_assume_aligned (x, 32);
y = (float*)__builtin_assume_aligned (y, 32);
float sum = 0;
for(int i=0; i<n; i++) {
sum += x[i]*y[i];
}
return sum;
}
int main(){
uint64_t LEN = 1 << 27;
float *x = (float*)_mm_malloc(sizeof(float)*LEN,64);
float *y = (float*)_mm_malloc(sizeof(float)*LEN,64);
for(uint64_t i=0; i<LEN; i++) { x[i] = 1.0*rand()/RAND_MAX - 0.5; y[i] = 1.0*rand()/RAND_MAX - 0.5;}
uint64_t size = 2*sizeof(float)*LEN;
volatile float sum = 0;
double dtime, rate, flops;
int repeat = 100;
dtime = omp_get_wtime();
for(int i=0; i<repeat; i++) sum += dot(x,y,LEN);
dtime = omp_get_wtime() - dtime;
rate = 1.0*repeat*size/dtime*1E-9;
flops = 2.0*repeat*LEN/dtime*1E-9;
printf("%f GB, sum %f, time %f s, %.2f GB/s, %.2f GFLOPS\n", 1.0*size/1024/1024/1024, sum, dtime, rate,flops);
sum = 0;
dtime = omp_get_wtime();
for(int i=0; i<repeat; i++) sum += dot_avx(x,y,LEN);
dtime = omp_get_wtime() - dtime;
rate = 1.0*repeat*size/dtime*1E-9;
flops = 2.0*repeat*LEN/dtime*1E-9;
printf("%f GB, sum %f, time %f s, %.2f GB/s, %.2f GFLOPS\n", 1.0*size/1024/1024/1024, sum, dtime, rate,flops);
}
我刚刚按照 Jonathan Dursi 的建议下载、编译并运行了 STREAM,结果如下:
一个线程
Function Rate (MB/s) Avg time Min time Max time
Copy: 14292.1657 0.0023 0.0022 0.0023
Scale: 14286.0807 0.0023 0.0022 0.0023
Add: 14724.3906 0.0033 0.0033 0.0033
Triad: 15224.3339 0.0032 0.0032 0.0032
八个线程
Function Rate (MB/s) Avg time Min time Max time
Copy: 24501.2282 0.0014 0.0013 0.0021
Scale: 23121.0556 0.0014 0.0014 0.0015
Add: 25263.7209 0.0024 0.0019 0.0056
Triad: 25817.7215 0.0020 0.0019 0.0027
【问题讨论】:
-
您有多少物理 CPU?你的记忆通道是如何填充的?
-
我希望你在某个时候写下整个项目。在这里,问题只是一个线程并没有完全使内存子系统饱和——这并不一定等于说单线程性能仍有提升空间。通过预取,并且一次有多个内存请求在运行,可能会有一些操作数准备好进行点积,但不是第一个线程所期望的。您可能已经看过 this ref - 它现在有点旧但很全面。
-
@JonathanDursi,我想我需要阅读“每个程序员应该了解的关于内存的知识”。我过去曾尝试过几次,但它有 114 页...
-
我将尝试将其中的一些对话提炼成答案...
-
我还发现内存带宽更难预测和测量。首先,您在读取和写入带宽之间有明显的区别。在某些系统上,您可以获得两者的全部带宽,因为它们使用不同的通道。那么你是否流式传输也很重要。如果您不流式写入,它们也会产生读取成本。与缓存和其他内部 CPU 瓶颈不同,扩大对带宽的需求不会导致性能图中的“悬崖”。相反,您会看到平稳的递减收益。
标签: c++ memory openmp bandwidth avx