流内存带宽基准测试的预期结果

流内存带宽基准测试的预期结果

我最近组装了一个 Threadripper 系统,并stream在其上运行了内存带宽基准测试。结果远低于系统的理论带宽,我不确定这些传输速度是否正常,或者它们是否表明系统中存在一些配置问题。

规格如下:

CPU: Threadripper PRO 3995WX
Motherboard: Asus Pro WS WRX80E-SAGE SE WIFI
RAM: 8 sticks of 3200MHz 64GB DDR4 RAM (I can provide more detailed information if necessary)
OS: RHEL 9.2

stream使用以下命令编译了基准测试。我改变了数组大小和线程数,这些似乎比其他配置的结果略好一些。

gcc -O -DSTREAM_ARRAY_SIZE=1000000000 -mcmodel=medium -fopenmp -D_OPENMP stream.c -o stream
export OMP_NUM_THREADS=16
./stream

结果是

Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:          103566.6     0.154768     0.154490     0.155157
Scale:         103404.6     0.155270     0.154732     0.157047
Add:           112979.4     0.212911     0.212428     0.213778
Triad:         113143.5     0.212315     0.212120     0.212602

这个结果似乎令人失望,因为这大约是 8 通道 3200MHz DDR4 理论带宽的一半(根据本网站)。我使用 AOCC 而不是 gcc 进行编译,这大大提高了基准测试的性能Copy

(testing with array size 10,000,000,000 instead since that gives better numbers)
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:          163585.1     0.979134     0.978084     0.980080
Scale:         102983.4     1.554170     1.553648     1.555248
Add:           113216.5     2.120998     2.119832     2.123423
Triad:         113144.9     2.122118     2.121173     2.123545

虽然Copy已经非常接近实际传输速度极限,但其他测试仍然远低于预期。我还认为,也许最后 3 次测试中的数据传输是双向的,导致带宽大约减少了一半。但是,将基准测试结果加倍会产生高于理论极限的传输速度,这应该是不可能的。

最佳结果网站上列出的stream基准测试已经很老了,而且主要适用于超级计算机,我很难在网上找到类似的基准测试。如果有人知道如何解释这些stream基准测试的结果,我将不胜感激。

答案1

总结

您的 STREAM 基准测试结果看起来不错,但需要仔细解释才能得出正确的结论。在您的特定情况下(通常使用 GCC 时都是这种情况),要获得实际内存带宽,测试Scale结果必须乘以 1.5 倍,并且AddTriad测试必须乘以 1.33 倍。

在我们继续之前,请注意,这些乘数仅应在 的速度Copy明显快于ScaleAdd时使用Triad。在其他情况下,需要逐案分析。现在回到主题。

有了这些校正系数,您的测试结果将是:

  • 复制:163585.1 x 1.0 = 163585.1 MB/s
  • 比例:102983.4 x 1.5 = 154475.1 MB/s
  • 添加:113216.5 x 1.33 = 150577.945 MB/s
  • 三元组:113144.9 x 1.33 = 150482.717 MB/s

现在,这是一个自洽的结果。现在,作为另一个现实检查,考虑到 8 通道 DDR4 @ 3200 MT/s 的理论带宽为 204.8 GB/s,峰值复制带宽为 163.5 GB/s,这表明您的 CPU 运行在其理论内存带宽的近 80%。对于 Triad,其效率约为 73.4%。两者都是典型结果,因此您的 CPU 运行正常。

带宽比较注意事项:我个人的经验是,其他在 PC 爱好者中流行的内存基准测试工具可能会报告更高的数字。特别是,AIDA64 经常报告非常接近硬件理论最大值的数字。由于它是专有的,因此测量方法如何进行,谁也说不准。我怀疑他们使用高度调整的汇编内核和线程安排来接触尽可能多的缓存行。另一方面,STREAM 测试更能反映实际内存受限应用程序的性能上限。因此,不应将 AIDA64 的结果直接与 STREAM 进行比较。另一方面,SiSoftware Sandra 的内存带宽测试似乎与 STREAM 类似,因此您可以将 STREAM 测试与偶尔出现在在线评论中的 SiSoft Sandra 内存带宽测试进行比较。

请注意,按照惯例,在几乎所有情况下,内存带宽都是以 GB/s 为单位,而不是 GiB/s,除非另有说明(因为它也经常与 GFLOPS 一起使用,因此使用相同的十进制 SI 前缀更方便)。

讨论

STREAM 是一个简单、粗糙但有效的 100 行 C 程序,旨在供超级计算和高性能计算领域的程序员使用。如果您不具备相关技术细节的先验知识,则很容易误解测试。它不是面向公众的自动且万无一失的基准测试系统,您不能只按一个按钮就期望看到正确的结果。

以下是我个人的解释。但要了解更多信息,请查看以下参考资料。参考资料包括我在下面的答案中没有提到的一些要点,例如 NUMA 亲和性。

了解内存和缓存

要了解魔法数字的起源1.51.33首先需要了解写入分配非临时存储在 CPU 的内存和缓存子系统的上下文中。

可以归结为以下事实:

  1. 缓存线:在大多数现代 CPU 上,所有内存读写都是在缓存行的基本单位上完成的 - 通常是 64 字节。

  2. 写分配:访问内存时,通常首先将整个缓存行读入 CPU 的最后一级缓存,包括只写内存流量。内存写入请求通常秘密地是读取 + 写入请求。这样做是因为大多数内存访问都表现出时间局部性- 很有可能,您将重新使用刚刚写入的数据,因此最好发出额外的读取以将其带入缓存。

  3. 非临时存储:如果你的内存访问没有时间局部性(换句话说,在数据写入内存后,你不会很快重新使用它),该怎么办?对于专家来说,大多数 CPU 都以特殊的形式提供了一个逃生舱非时间性的汇编指令。当发出非临时存储指令时,CPU 会将所涉及的缓存行直接写入内存,完全绕过缓存。在 x86 上,SSE 指令MOVNTDQ用于此目的,它被称为流媒体商店

  4. memcpy():大多数编译器不支持自动使用这些非时间性的指令(即​​使是英特尔icc也不会这样做,除非明确要求这样做)。另一方面,memcpy()通常由专家以手写汇编代码为特定架构实现,对于大型内存复制,通常使用非临时存储来提高性能。大多数编译器还可以自动将memcpy()类似循环转换为 true memcpy()。因此,复制内存通常是最快的内存操作。

理解STREAM内核

有了这些知识,让我们来检查一下 4 个 STERAM 内核的源代码。

复制

复制内核极其简单。

#pragma omp parallel for
        for (j=0; j<STREAM_ARRAY_SIZE; j++)
            c[j] = a[j];

此循环将数组中的每个元素复制a到数组中c,因此此处的流量是 1 次内存读取和 1 次内存写入。换句话说,它执行以下计算:

dst[n] = src[n]

对于大多数现代编译器来说,它们可以自动将此循环转换为 的优化版本memcpy(),通常是针对 CPU 架构手动优化的汇编语言。这些优化memcpy通常使用非临时存储指令,例如 x86 SSE 的MOVNTDQ。因此,在写入数组 时不会产生额外的读取流量c

这就是为什么STREAM经常显示最快的结果Copy

另一方面,如果编译器没有memcpy()自动执行优化(例如,对于 GCC/clang,可以使用选项-fno-builtin),由于大多数 CPU 上的写分配策略,实际流量将是 2 次读取和 1 次写入,并且应该将结果乘以一个因子:

(2 + 1) / (1 + 1) = 1.5

规模

Scale内核极其简单。

#pragma omp parallel for
    for (j=0; j<STREAM_ARRAY_SIZE; j++)
        b[j] = scalar*c[j];

它将数组中的每个元素乘以一个常数,并将结果写入新数组:

dst[n] = k * src[n]

此循环有 1 次内存读取、1 次浮点乘法和 1 次内存写入。由于写入分配策略,在大多数 CPU 上它实际上执行 2 次内存读取。因此,如果不使用非临时存储指令,内存带宽将被低估:

(2 + 1) / (1 + 1) = 1.5

添加

Add 内核极其简单。

#pragma omp parallel for
    for (j=0; j<STREAM_ARRAY_SIZE; j++)
        c[j] = a[j]+b[j];

它将两个元素相加并将结果写入另一个数组的新元素中:

dst[i] = src1[i] + src2[i]

该循环有 2 次内存读取、1 次浮点加法和 1 次内存写入。由于写入分配策略,在大多数 CPU 上它实际上执行 3 次内存读取。因此,应将结果乘以以下因子:

(3 + 1) / (2 + 1) = 1.33

三合会

Triad 内核极其简单。

#pragma omp parallel for
    for (j=0; j<STREAM_ARRAY_SIZE; j++)
        a[j] = b[j]+scalar*c[j];

它将一个数组元素与一个常数相乘,将其添加到另一个数组中的另一个元素中,并将结果写入最终数组的新元素中:

dst[i] = src1[i] + k * src2[i]

此循环有 2 次内存读取、1 次浮点乘法、1 次浮点加法和 1 次内存写入。由于写入分配策略,在大多数 CPU 上,实际上有 3 次内存读取,而不是 2 次。因此,应将结果乘以以下因子:

(3 + 1) / (2 + 1) = 1.33

这就是所有神奇数字的起源。

在 HPC 中,STREAM Triad 通常是 CPU 及其内存控制器的标准效率测试,许多研究论文都对此进行了报道。它使用最简单的软件(1 次读取、2 次写入和 1 次融合乘加)来测量硬件理论带宽与实际带宽之间的差距。

从经验上看,吞吐量约为 CPU 理论峰值的 80%。这大致代表了任何实际软件可以达到的最快速度。

为什么不STREAM自动更正其结果?

为什么不STREAM自动执行上述更正?

首先,因为它是一个用 C(和 Fortran)编写的高级程序,并且它注定要在许多硬件平台上使用,从单板计算机到超级计算机。不可能预测所有可能的编译器和所有系统的 CPU 的确切行为(例如是否使用非临时存储)。当然,clang 有__builtin_nontemporal_store(),但它是 clang 特定的并且不可移植。因此,最好如实地报告软件所看到的确切内容,而不做任何假设。解释留给读者作为练习。

例如,一个名为写分配逃避在某些智能手机使用的 ARM CPU 以及 Ice Lake 和较新的 Intel CPU 上可用。此功能允许 CPU 启发式地绕过初始缓存行读取,以在某些条件下节省内存带宽。这种优化在 Intel CPU 上称为“SpecI2M”,所涉及的确切启发式方法无人知晓,而且非常不一致:

因此,STREAM可能会在这些 CPU 上显示更高的带宽结果,但仅限于某些条件下。这将使任何预先应用的校正因子无效。再举一个例子,当使用 Intel C 编译器时,可以使用命令行选项强制启用非临时存储-qopt-streaming-stores always

其次,因为它是一个简单易懂的 100 行 C 程序,并且自 20 世纪 90 年代末以来一直是标准。任何使用自动检测的行为都会削弱其可信度,并可能使测试更加脆弱。

备择方案

由于 STREAM 容易出错的特性,我认为这项测试已经开始显露出它的过时之处。当然,STREAM 是经受住了时间考验的行业标准,人们可能仍想运行它来与其他人的基准进行比较。但由于与 NUMA、编译器和 CPU 的写入分配行为相关的陷阱太多 - 这些陷阱无法自动处理并且可能会产生误导性结果,因此是时候考虑一​​些替代方案了。

为了测试 CPU 内存带宽,我更喜欢使用 LIKWID。Likwid 是 HPC 应用程序性能调优的瑞士军刀。它的主要用途是从低级硬件性能计数器收集统计数据,以了解和优化代码的性能特征。但它还包括一个名为的微型基准测试工具likwid-bench

likwid-bench可以自动启动基准测试程序的多个实例,并将每个实例的进程和内存固定到其对应的 NUMA 节点。它还提供许多预先编写的基准测试内核(包括 STREAM Triad 基准测试的等效项),这些内核以手写汇编语言编写,可实现准确一致的结果,例如:

$ likwid-bench -a
copy - Double-precision vector copy, only scalar operations
copy_avx - Double-precision vector copy, optimized for AVX
copy_avx512 - Double-precision vector copy, optimized for AVX-
copy_mem - Double-precision vector copy, only scalar operations but with non-temporal stores
copy_mem_avx - Double-precision vector copy, uses AVX and non-temporal stores
copy_mem_avx512 - Double-precision vector copy, uses AVX-
copy_mem_sse - Double-precision vector copy, uses SSE and non-temporal stores
copy_sse - Double-precision vector copy, optimized for SSE
stream - Double-precision stream triad A(i) = B(i)*c + C(i), only scalar operations
stream_avx - Double-precision stream triad A(i) = B(i)*c + C(i), optimized for AVX
stream_avx512 - Double-precision stream triad A(i) = B(i)*c + C(i), optimized for AVX-
stream_avx512_fma - Double-precision stream triad A(i) = B(i)*c + C(i), optimized for AVX-
stream_avx_fma - Double-precision stream triad A(i) = B(i)*c + C(i), optimized for AVX FMAs
stream_mem - Double-precision stream triad A(i) = B(i)*c + C(i), uses SSE and non-temporal stores
stream_mem_avx - Double-precision stream triad A(i) = B(i)*c + C(i), uses AVX and non-temporal stores
stream_mem_avx512 - Double-precision stream triad A(i) = B(i)*c + C(i), uses AVX-
stream_mem_avx_fma - Double-precision stream triad A(i) = B(i)*c + C(i), optimized for AVX FMAs and non-temporal stores
stream_mem_sse - Double-precision stream triad A(i) = B(i)*c + C(i), uses SSE and non-temporal stores
stream_mem_sse_fma - Double-precision stream triad A(i) = B(i)*c + C(i), uses SSE FMAs and non-temporal stores

下面是使用 STREAM Triad 基准测量内存带宽的示例likwid-bench,使用 AVX、FMA 和非临时存储,使用 4 个实例,每个实例都最佳地固定到 NUMA 节点。

作为参考,这是一台双插槽 Intel Xeon E5-2680 v4 机器,每个插槽有 4 通道 DDR4 ECC RDIMM @ 2400 MT/s,因此它应该表现得像一台 8 通道机器。片上集群模式在 BIOS 中启用,因此,每个 CPU 被视为两个 NUMA 节点 - CPU 的硅片在物理上被划分为 2 个部分,每个部分都有自己的 2 通道内存控制器,通过两个独立的环形总线之间的桥接器相互连接,每个环形总线都有一半的内核。整个机器有 4 个 NUMA 节点。对于支持 NUMA 的应用程序,这提供了最高的内存带宽。

$ likwid-bench -t stream_mem_avx_fma -w M0:1GB -w M1:1GB -w M2:1GB -w M3:1GB
--------------------------------------------------------------------------------
Cycles:                 22089219858
CPU Clock:              2394502471
Cycle Clock:            2394502471
Time:                   9.224973e+00 sec
Iterations:             14336
Iterations per thread:  256
Inner loop executions:  186011
Size (Byte):            3999980544
Size per thread:        71428224
Number of Flops:        85332918272
MFlops/s:               9250.21
Data volume (Byte):     1023995019264
MByte/s:                111002.50
Cycles per update:      0.517719
Cycles per cacheline:   4.141749
Loads per update:       2
Stores per update:      1
Load bytes per element: 16
Store bytes per elem.:  8
Load/store ratio:       2.00
Instructions:           39999805457
UOPs:                   58666381312

根据likwid-bench,这台机器的 STREAM Triad 性能为 111 GB/s。现在将结果与原始 STREAM 基准进行比较:

-------------------------------------------------------------
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:           96094.9     0.017320     0.016650     0.030050
Scale:          74934.2     0.022183     0.021352     0.033086
Add:            84007.2     0.029319     0.028569     0.041626
Triad:          85827.9     0.028947     0.027963     0.039657
-------------------------------------------------------------

将 85827.9 MB/s 乘以 1.33x 等于 114149.91 MB/s。这表明两个结果是一致的,且在误差范围内。

最后,请注意,8 通道 DDR4 2400 MT/s 的理论带宽为 153.6 GB/s。因此,这款英特尔 Broadwell-EP CPU 的硬件效率为 72.2% - 有点偏低。对于服务器 CPU 来说,这并不奇怪。

相关内容