问题 快速(est)方式将整数序列写入全局内存?


任务非常简单,将整数变量写入内存:

原始代码:

for (size_t i=0; i<1000*1000*1000; ++i)
{
   data[i]=i;
};

并行化代码:

    size_t stepsize=len/N;

#pragma omp parallel num_threads(N)
    {
        int threadIdx=omp_get_thread_num();

        size_t istart=stepsize*threadIdx;
        size_t iend=threadIdx==N-1?len:istart+stepsize;
#pragma simd
        for (size_t i=istart; i<iend; ++i)
            x[i]=i;
    };

性能很糟糕,需要 1.6秒 写1G uint64 变量(每秒等于5GB),通过简单的并行化(open mp parallel)上面的代码,速度提升abit,但性能仍然很糟糕,采取 1.4秒 在i7 3970上有4个螺纹和1.35个带6个螺纹。

我的装备的理论记忆带宽(i7 3970 / 64G DDR3-1600)是 51.2 GB /秒,对于上面的例子,实现的存储器带宽仅约为 1/10 即使通过应用程序,理论带宽几乎与内存带宽有关。

有谁知道如何改进代码?

我在GPU上编写了很多内存绑定代码,GPU很容易充分利用GPU的设备内存带宽(例如,带宽的85%以上)。

编辑:

该代码由Intel ICC 13.1编译为64位二进制文​​件,并具有最大优化(O3)和AVX代码路径,以及自动矢量化。

更新:

我尝试了下面的所有代码(感谢Paul R),没有什么特别的事情发生,我相信编译器完全有能力进行simd /矢量化优化。

至于我为什么要在那里填写数字,好吧,长话短说:

它是高性能异构计算算法的一部分,在设备方面,algorthim非常高效,以至于多GPU集如此之快,以至于我发现性能瓶颈恰好是当CPU尝试写几个序列时数字到记忆。

原因是,知道CPU吸收填充数字(相比之下,GPU可以非常接近的速度填充数量的数量(238GB /秒 在......之外 288GB /秒 在GK110与可悲的 5GB /秒 在......之外 51.2GB /秒 在CPU上)到GPU的全局内存的理论带宽),我可以稍微改变一下我的algorthim,但是让我想知道为什么CPU在这里填充数量方面非常糟糕。

至于我的装备的内存带宽,我相信带宽(51.2GB)是正确的,基于我的 memcpy() 测试,实现带宽约 80%+ 理论带宽(> 40GB /秒)。


5583
2017-08-23 13:01


起源

你尝试过优化代码吗?例如。如果您使用,请使用-O3 gcc? - Mats Petersson
@unwind Mohammed这就是编译器所做的事情。如果汇编代码表明编译器在这方面做得不好,那么很好,但是在dubio pro编译器中;-) OP,你能显示生成的汇编吗?
@delnan很可能。是时候打电话给Mysticial~ - Mohamad Ali Baydoun
房间里的大象当然是:为什么你(想你)需要记忆充满增加的整数序列?! - sehe
你从哪里获得理论带宽? - Joni


答案:


假设这是x86,并且您尚未使可用的DRAM带宽饱和,则可以尝试使用SSE2或AVX2一次写入2个或4个元素:

SSE2:

#include "emmintrin.h"

const __m128i v2 = _mm_set1_epi64x(2);
__m128i v = _mm_set_epi64x(1, 0);

for (size_t i=0; i<1000*1000*1000; i += 2)
{
    _mm_stream_si128((__m128i *)&data[i], v);
    v = _mm_add_epi64(v, v2);
}

AVX2:

#include "immintrin.h"

const __m256i v4 = _mm256_set1_epi64x(4);
__m256i v = _mm256_set_epi64x(3, 2, 1, 0);

for (size_t i=0; i<1000*1000*1000; i += 4)
{
    _mm256_stream_si256((__m256i *)&data[i], v);
    v = _mm256_add_epi64(v, v4);
}

注意 data 需要适当对齐(16字节或32字节边界)。

AVX2仅适用于Intel Haswell及更高版本,但SSE2目前非常普及。


FWIW我把一个带有标量循环的测试工具放在一起,上面的SSE和AVX循环用clang编译它,并在Haswell MacBook Air(1600MHz LPDDR3 DRAM)上进行测试。我得到了以下结果:

# sequence_scalar: t = 0.870903 s = 8.76033 GB / s
# sequence_SSE: t = 0.429768 s = 17.7524 GB / s
# sequence_AVX: t = 0.431182 s = 17.6941 GB / s

我也尝试在具有3.6 GHz Haswell的Linux台式PC上,使用gcc 4.7.2进行编译,并得到以下信息:

# sequence_scalar: t = 0.816692 s = 9.34183 GB / s
# sequence_SSE: t = 0.39286 s = 19.4201 GB / s
# sequence_AVX: t = 0.392545 s = 19.4357 GB / s

因此,看起来SIMD实现比64位标量代码提供了2倍或更多的改进(尽管256位SIMD似乎没有对128位SIMD进行任何改进),并且典型的吞吐量应该比5 GB /更快秒。

我的猜测是OP的系统或基准测试代码有问题导致吞吐量明显降低。


11
2017-08-23 13:13



你有没有...对它进行基准测试以确定它是否存在 其实 更快? - sehe
这是留给读者的练习,当然它取决于各种因素。但由于据称在OP的情况下DRAM带宽几乎没有达到饱和,我预计会有适度的改善。 - Paul R
@ user0002128:如果您让我知道您的特定编译器的错误,我可以尝试修复它们。至于自动矢量化 - 我怀疑即使是ICC也会对此进行矢量化,因为它不适合任何标准的自动矢量模型,但有一种简单的方法可以找到...... - Paul R
@PaulR上次我查了一下, -O2 内联 memcpy() 完全。
@ H2CO3:我知道 - 所有语言律师和学生都在桥上 - 他们不允许进入机房。 ;-) - Paul R


有什么理由你会期望所有的 data[] 在加电的RAM页面?

DDR3预取程序将正确预测大多数访问,但频繁的x86-64页边界可能是一个问题。您正在写入虚拟内存,因此在每个页面边界处可能会对预取器进行错误预测。您可以通过使用大页面(例如, MEM_LARGE_PAGES 在Windows上)。


5
2017-08-23 14:57



+1,OP的Sandybridge-E没有下一页预取。 Ivybridge和后来做的,这有助于一些。 IDK为什么使用所有6个核心并不是饱和内存BW。也许pagefaults + TLB未命中解释了它。单核不能使最近的英特尔芯片上的内存带宽饱和,因此您需要多个内核高效运行: 为memcpy增强了REP MOVSB 和 为什么Skylake比Broadwell-E在单线程内存吞吐量方面要好得多?。 - Peter Cordes