两个数组的点积
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
答案 0 :(得分:13)
这里有一些事情要归结为:
第一个有助于解释为什么需要多个线程来使可用内存带宽饱和。内存系统中存在大量并发性,并且利用它通常需要CPU代码中的一些并发性。多个执行线程帮助的一个重要原因是latency hiding - 当一个线程停止等待数据到达时,另一个线程可能能够利用刚才可用的其他一些数据。
在这种情况下,硬件可以在单个线程上为您提供很多帮助 - 因为内存访问是如此可预测,硬件可以在您需要时预先获取数据,从而为您提供延迟隐藏的一些优势,即使只有一个线;但是预取可以做什么是有限的。例如,预取器不会让自己跨越页面边界。大部分内容的规范参考是What Every Programmer Should Know About Memory by Ulrich Drepper,现在已经足够大,以至于一些差距开始显现(英特尔对Sandy Bridge处理器的Hot Chips概述为here - 特别注意内存管理硬件与CPU的紧密集成。)
关于与memset,mbw或STREAM进行比较的问题,在基准测试中进行比较总会引起令人头疼的问题,甚至是声称测量相同事物的基准测试。特别是&#34;内存带宽&#34;不是单个数字 - 性能因操作而有很大差异。 mbw和Stream都执行某种版本的复制操作,此处拼写了STREAMs操作(直接从网页中获取,所有操作数都是双精度浮点数):
------------------------------------------------------------------
name kernel bytes/iter FLOPS/iter
------------------------------------------------------------------
COPY: a(i) = b(i) 16 0
SCALE: a(i) = q*b(i) 16 1
SUM: a(i) = b(i) + c(i) 24 1
TRIAD: a(i) = b(i) + q*c(i) 24 2
------------------------------------------------------------------
因此在这些情况下大约1 / 2-1 / 3的存储器操作是写入(并且在memset的情况下所有内容都是写入)。虽然单个写入可能比读取慢一点,但更大的问题是使用写入使内存子系统饱和要困难得多,因为当然您无法完成预取写操作。交错读取和写入有所帮助,但是基本上所有读取的点积示例将是针对内存带宽的最佳可能情况。
此外,STREAM基准测试是(有意)完全可移植地编写的,只有一些编译器编译指示来建议矢量化,因此击败STREAM基准测试并不一定是一个警告信号,特别是当你正在做的事情时是两个流式读取。
答案 1 :(得分:3)
我制作了自己的内存基准代码https://github.com/zboson/bandwidth
以下是八个主题的当前结果:
write: 0.5 GB, time 2.96e-01 s, 18.11 GB/s
copy: 1 GB, time 4.50e-01 s, 23.85 GB/s
scale: 1 GB, time 4.50e-01 s, 23.85 GB/s
add: 1.5 GB, time 6.59e-01 s, 24.45 GB/s
mul: 1.5 GB, time 6.56e-01 s, 24.57 GB/s
triad: 1.5 GB, time 6.61e-01 s, 24.37 GB/s
vsum: 0.5 GB, time 1.49e-01 s, 36.09 GB/s, sum -8.986818e+03
vmul: 0.5 GB, time 9.00e-05 s, 59635.10 GB/s, sum 0.000000e+00
vmul_sum: 1 GB, time 3.25e-01 s, 33.06 GB/s, sum 1.910421e+04
以下是1个线程的电流结果:
write: 0.5 GB, time 4.65e-01 s, 11.54 GB/s
copy: 1 GB, time 7.51e-01 s, 14.30 GB/s
scale: 1 GB, time 7.45e-01 s, 14.41 GB/s
add: 1.5 GB, time 1.02e+00 s, 15.80 GB/s
mul: 1.5 GB, time 1.07e+00 s, 15.08 GB/s
triad: 1.5 GB, time 1.02e+00 s, 15.76 GB/s
vsum: 0.5 GB, time 2.78e-01 s, 19.29 GB/s, sum -8.990941e+03
vmul: 0.5 GB, time 1.15e-05 s, 468719.08 GB/s, sum 0.000000e+00
vmul_sum: 1 GB, time 5.72e-01 s, 18.78 GB/s, sum 1.910549e+04
memset
。a(i) = b(i) * c(i)
sum += a(i)
sum *= a(i)
sum += a(i)*b(i)
//点积我的结果与STREAM一致。我获得vsum
的最高带宽。 vmul
方法当前不起作用(一旦值为零,它就会提前完成)。我可以使用内在函数并展开我稍后将添加的循环,获得略微更好的结果(大约10%)。