使用 C 测量内存写入带宽

Posted

技术标签:

【中文标题】使用 C 测量内存写入带宽【英文标题】:Measure memory write bandwidth using C 【发布时间】:2021-11-20 01:27:54 【问题描述】:

我正在尝试测量我的内存的写入带宽,我创建了一个 8G 字符数组,并使用 128 个线程在其上调用 memset。下面是代码sn-p。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <pthread.h>
int64_t char_num = 8000000000;
int threads = 128;
int res_num = 62500000;

uint8_t* arr;

static inline double timespec_to_sec(struct timespec t)

    return t.tv_sec * 1.0 + t.tv_nsec / 1000000000.0;


void* multithread_memset(void* val) 
    int thread_id = *(int*)val;
    memset(arr + (res_num * thread_id), 1, res_num);
    return NULL;


void start_parallel()

    int* thread_id = malloc(sizeof(int) * threads);
    for (int i = 0; i < threads; i++) 
        thread_id[i] = i;
    
    pthread_t* thread_array = malloc(sizeof(pthread_t) * threads);
    for (int i = 0; i < threads; i++) 
        pthread_create(&thread_array[i], NULL, multithread_memset, &thread_id[i]);
    
    for (int i = 0; i < threads; i++) 
        pthread_join(thread_array[i], NULL);
    


int main(int argc, char *argv[])

    struct timespec before;
    struct timespec after;
    float time = 0;
    arr = malloc(char_num);

    clock_gettime(CLOCK_MONOTONIC, &before);
    start_parallel();
    clock_gettime(CLOCK_MONOTONIC, &after);
    double before_time = timespec_to_sec(before);
    double after_time = timespec_to_sec(after);
    time = after_time - before_time;
    printf("sequential = %10.8f\n", time);
    return 0;

根据输出,完成所有 memset 需要 0.6 秒,据我了解,这意味着 8G/0.6 = 13G 内存写入带宽。但是,我有一个 2667 MHz DDR4,它应该有 21.3 GB/s 的带宽。我的代码或计算有什么问题吗?感谢您的帮助!

【问题讨论】:

您假设所有线程都在不同的 CPU 上运行,并且所有线程都受 CPU 限制。而且,您只提供了一位小数点的精度。所以 0.6 可能是 0.550 到 0.649 或 12.3 GB/s 到 14.5 GB/s 之间的任何值。因此,仅测量到小数点后会产生超过 2 GB/s 的变化。 一方面,memset 不会只写周期。每个缓存行中的第一个写指令必然会将该行读入缓存,因为 CPU 不知道您稍后会覆盖所有它。 另外,128 个线程很多,除非你有 128 个内核。在它们之间切换上下文所花费的时间可能很重要。 8e10 不是 8G。 8G为8*1024*1024*1024 如果你想防止将缓存行读入 CPU 缓存,你可能想看看non-temporal writes。您不必为此编写汇编代码。你也可以使用compiler intrinsics。 【参考方案1】:

TL;DR:测量内存带宽并不容易。在您的情况下,性能问题可能来自 页面错误

如果要测量内存写入带宽,需要注意多个方面:

在 Intel/AMD x86 平台上,未在缓存中获取的位置的内存写入会导致 write allocate:未写入位置的数据被加载到缓存中。请参阅this page 了解更多信息。该策略使处理器能够填充缓存行中未写入的部分,以确保 CPU 缓存的一致性。然而,这也意味着一半的内存吞吐量被“浪费”了。在实践中,情况甚至更糟,因为交错内存读写通常会引入额外的开销。解决此问题的一种解决方案是使用非临时写入指令。在SSE 中,您可以使用_mm_stream_* 内在函数(通常是_mm_stream_si128)。在AVX 中,这是_mm256_stream_* 内在函数(通常是_mm256_stream_si256)。请注意,仅当数据块不适合缓存或此后不久未重用时,才可以使用此类指令。一个好的 libc 实现应该在大块上使用 memsetmemcpy 这样的指令。

大多数操作系统实际上并没有在分配时将分配的页面映射到物理页面。内存只是虚拟分配而不是物理分配。对分配的内存页面进行first touch会导致page fault,这非常昂贵。整个页面通常在此时进行物理映射,并且出于安全原因,在大多数系统上它重置为零。为了测量内存吞吐量,您无需在基准测试中包含这样的开销,只需预先分配内存并提前写入内存块(如果可能,使用随机值)。

CPU 缓存可能非常大,写入的内存缓冲区应该比它们大得多,以免衡量缓存本身的吞吐量(通常是由于缓存关联性)。

一个线程通常不足以使主内存的带宽饱和。通常很少需要达到最佳吞吐量(这非常依赖于平台,在英特尔至强处理器等服务器处理器上通常需要很多线程)。如果线程过多,可能会出现一些复杂的影响(例如争用),从而降低整体吞吐量。

在NUMA systems 上,如果内核访问自己的内存,内存访问通常会更快。这意味着线程应该固定到核心,并且应该读/写到线程专用的缓冲区,以实现最佳吞吐量。例如,在 AMD Ryzen 桌面/服务器处理器或双路服务器系统上尤其如此。

现代处理器通常使用可变频率(请参阅frequency scaling)。此外,线程可能需要一些时间来创建和实际启动。因此,在具有同步屏障的同一缓冲区上使用循环多次迭代对于最小化由此效应引入的偏差非常重要。这对于检查每个线程所花费的时间是否大致相同也很重要(否则,这意味着会发生像 NUMA 一样的不良影响)。

使用的内存量不应该太大,因为某些操作系统使用内存压缩策略(例如z-swap)来避免内存使用太大。在最坏的情况下,可以使用交换存储设备。

请注意,您可以使用 OpenMP 更轻松地编写并行代码(生成的代码会更小且更易于阅读)。 OpenMP 还使您能够控制线程锁定并根据目标体系结构对适量的线程进行线程化。大多数编译器都支持 OpenMP,包括 GCC、Clang、ICC、MSVC(目前只有 MSVC 的 2.0 版)。

【讨论】:

以上是关于使用 C 测量内存写入带宽的主要内容,如果未能解决你的问题,请参考以下文章

如何测量带宽使用情况

是否可以使用 ping 测量带宽?

如何计算理论上的最大 CPU-RAM 带宽?

使用speedtest-cli测量服务器带宽

内存带宽使用

NUMA 会影响内存带宽,还是只是延迟?