【问题标题】:Fast(est) way to write a seqence of integer to global memory?将整数序列写入全局内存的快速(est)方法?
【发布时间】:2013-08-26 13:10:05
【问题描述】:

任务很简单,将一串整型变量写入内存:

原码:

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;
    };

性能糟透了,写1G uint64变量(相当于每秒5GB)需要1.6秒,通过上面代码的简单并行化(open mp parallel),速度提高了一点,但性能仍然很差,在 i7 3970 上使用 4 个线程需要 1.4 秒,使用 6 个线程需要 1.35 秒。

我的设备(i7 3970/64G DDR3-1600)的理论内存带宽是51.2 GB/sec,对于上面的例子,实现的内存带宽只有大约是理论带宽的 1/10,即使应用程序几乎受内存带宽限制。

有人知道如何改进代码吗?

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

编辑:

代码由 Intel ICC 13.1 编译为 64 位二进制,并启用了最大优化 (O3) 和 AVX 代码路径,以及自动矢量化。

更新:

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

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

它是高性能异构计算算法的一部分,在设备端,该算法效率很高,以至于多GPU集如此之快,以至于我发现性能瓶颈恰好在CPU尝试将多个数字序列写入内存。

当然,知道 CPU 填充数字很糟糕(相比之下,GPU 可以以非常接近的速度填充数字序列(238GB/秒 288GB/秒 strong> GK110 与可怜的 5GB/sec(CPU 上 51.2GB/sec)到 GPU 全局内存的理论带宽),我可以稍微改变我的算法,但让我想知道的是为什么 CPU 在这里填充数字序列时如此糟糕。

至于我的设备的内存带宽,我认为带宽(51.2GB)是正确的,根据我的memcpy()测试,实现的带宽约为理论带宽的80%+>40GB/秒)。

【问题讨论】:

  • 您是否尝试过优化代码?例如。如果您使用的是gcc,请使用-O3?
  • @unwind Mohammed 这就是编译器所做的事情。如果汇编代码表明编译器在这方面做得不好,那很好,但是在 dubio pro 编译器中 ;-) OP,你能显示生成的汇编吗?
  • @delnan 很有可能。是时候打电话给神秘人了~
  • 房间里的大象当然是:为什么你(认为你)需要用递增的整数序列填充内存?!
  • 你从哪里得到理论带宽?

标签: c++ c memory optimization


【解决方案1】:

假设这是 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 台式电脑上进行了尝试,使用 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 的系统或基准测试代码存在问题,导致吞吐量明显降低。

【讨论】:

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

您是否有任何理由认为所有data[] 都位于已启动的 RAM 页面中?

DDR3 预取器可以正确预测大多数访问,但频繁的 x86-64 页面边界可能是一个问题。您正在写入虚拟内存,因此在每个页面边界都存在对预取器的潜在错误预测。您可以通过使用大页面(例如 Windows 上的MEM_LARGE_PAGES)大大减少这种情况。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2021-06-06
  • 1970-01-01
  • 2013-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多