1

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

#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 的带宽。我的代码或计算有什么问题吗?谢谢你的帮助!!

4

1 回答 1

4

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

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

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

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

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

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

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

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

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

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

于 2021-09-28T12:22:35.920 回答