【问题标题】:Random mmaped memory access up to 16% slower than heap data access随机映射内存访问比堆数据访问慢 16%
【发布时间】:2017-10-15 12:33:54
【问题描述】:

我们的软件在内存中构建了一个大约 80 GB 的数据结构。然后它可以直接使用此数据结构进行计算,也可以将其转储到磁盘以便之后可以重复使用多次。在这个数据结构中发生了很多随机内存访问。

对于更大的输入,此数据结构可能会变得更大(我们最大的一个超过 300 GB)并且我们的服务器有足够的内存来保存 RAM 中的所有内容。

如果数据结构被转储到磁盘,它会被 mmap 加载回地址空间,强制进入 os 页面缓存,最后被 mlocked(代码在末尾)。

问题在于,在堆上立即使用计算数据结构(参见 Malloc 版本)或映射转储文件(参见 mmap 版本)之间的性能差异大约为 16%。 我没有很好的解释为什么会这样。有没有办法找出为什么 mmap 这么慢?我能以某种方式缩小这种性能差距吗?

我在运行 Scientific Linux 7.2 和 3.10 内核的服务器上进行了测量,它有 128GB RAM(足以容纳所有东西),并重复了几次,结果相似。有时差距会小一点,但不会太大。

新更新(2017 年 5 月 23 日):

我制作了一个最小的测试用例,可以看到效果。我尝试了不同的标志(MAP_SHARED 等)但没有成功。 mmap 版本仍然较慢。

#include <random>
#include <iostream>
#include <sys/time.h>
#include <ctime>
#include <omp.h>
#include <sys/mman.h>
#include <unistd.h>

constexpr size_t ipow(int base, int exponent) {
    size_t res = 1;
    for (int i = 0; i < exponent; i++) {
        res = res * base;
    }
    return res;
}

size_t getTime() {
    struct timeval tv;

    gettimeofday(&tv, NULL);
    size_t ret = tv.tv_usec;
    ret /= 1000;
    ret += (tv.tv_sec * 1000);

    return ret;
}

const size_t N = 1000000000;
const size_t tableSize = ipow(21, 6);

size_t* getOffset(std::mt19937 &generator) {
    std::uniform_int_distribution<size_t> distribution(0, N);
    std::cout << "Offset Array" << std::endl;
    size_t r1 = getTime();
    size_t *offset = (size_t*) malloc(sizeof(size_t) * tableSize);
    for (size_t i = 0; i < tableSize; ++i) {
        offset[i] = distribution(generator);
    }
    size_t r2 = getTime();
    std::cout << (r2 - r1) << std::endl;

    return offset;
}

char* getData(std::mt19937 &generator) {
    std::uniform_int_distribution<char> datadist(1, 10);
    std::cout << "Data Array" << std::endl;
    size_t o1 = getTime();
    char *data = (char*) malloc(sizeof(char) * N);
    for (size_t i = 0; i < N; ++i) {
        data[i] = datadist(generator);  
    }
    size_t o2 = getTime();
    std::cout << (o2 - o1) << std::endl;

    return data;
}

template<typename T>
void dump(const char* filename, T* data, size_t count) {
    FILE *file = fopen(filename, "wb");
    fwrite(data, sizeof(T), count, file); 
    fclose(file);
}

template<typename T>
T* read(const char* filename, size_t count) {
#ifdef MMAP
    FILE *file = fopen(filename, "rb");
    int fd =  fileno(file);
    T *data = (T*) mmap(NULL, sizeof(T) * count, PROT_READ, MAP_SHARED | MAP_NORESERVE, fd, 0);
    size_t pageSize = sysconf(_SC_PAGE_SIZE);
    char bytes = 0;
    for(size_t i = 0; i < (sizeof(T) * count); i+=pageSize){
        bytes ^= ((char*)data)[i];
    }
    mlock(((char*)data), sizeof(T) * count);
    std::cout << bytes;
#else
    T* data = (T*) malloc(sizeof(T) * count);
    FILE *file = fopen(filename, "rb");
    fread(data, sizeof(T), count, file); 
    fclose(file);
#endif
    return data;
}

int main (int argc, char** argv) {
#ifdef DATAGEN
    std::mt19937 generator(42);
    size_t *offset = getOffset(generator);
    dump<size_t>("offset.bin", offset, tableSize);

    char* data = getData(generator);
    dump<char>("data.bin", data, N);
#else
    size_t *offset = read<size_t>("offset.bin", tableSize); 
    char *data = read<char>("data.bin", N); 
    #ifdef MADV
        posix_madvise(offset, sizeof(size_t) * tableSize, POSIX_MADV_SEQUENTIAL);
        posix_madvise(data, sizeof(char) * N, POSIX_MADV_RANDOM);
    #endif
#endif

    const size_t R = 10; 
    std::cout << "Computing" << std::endl;
    size_t t1 = getTime();
    size_t result = 0;
#pragma omp parallel reduction(+:result)
    {
        size_t magic = 0;
        for (int r = 0; r < R; ++r) {
#pragma omp for schedule(dynamic, 1000)
            for (size_t i = 0; i < tableSize; ++i) {
                char val = data[offset[i]];
                magic += val;
            }
        }
        result += magic;
    }
    size_t t2 = getTime();

    std::cout << result << "\t" << (t2 - t1) << std::endl;
}

请原谅 C++,它的随机类更容易使用。我是这样编译的:

#  The version that writes down the .bin files and also computes on the heap
g++ bench.cpp -fopenmp -std=c++14 -O3 -march=native -mtune=native -DDATAGEN
# The mmap version
g++ bench.cpp -fopenmp -std=c++14 -O3 -march=native -mtune=native -DMMAP
# The fread/heap version
g++ bench.cpp -fopenmp -std=c++14 -O3 -march=native -mtune=native
# For madvice add -DMADV

在这台服务器上我得到以下时间(运行所有命令几次):

./mmap
2030ms

./fread
1350ms

./mmap+madv
2030ms

./fread+madv
1350ms

numactl --cpunodebind=0 ./mmap 
2600 ms

numactl --cpunodebind=0 ./fread 
1500 ms

【问题讨论】:

  • 如我所见,fillWithData 以一大步读取整个文件。另一方面,无论您在何处访问文件,mmap 都会逐个读取文件。这可能会导致性能差异。为了更加现实,请重新运行基准测试,包括最后写入磁盘部分...
  • 您是否正在更新您mmap() 中的数据?如果是这样,第一次更新数据时,您会强制数据的内存副本将其后备存储从映射到的文件更改为由交换支持的匿名内存。这种映射变化需要时间。从malloc() 获得的内存不必在修改时交换其后备存储。 malloc() 也可以使用更大的页面大小来实现。 mmap() 不是灵丹妙药,在某些方面使用时会出现严重的性能问题。阅读this from one Linus Torvalds.
  • @Brutos 什么文件系统?您可以尝试使用带有 MAP_HUGETLBMAP_HUGE_2MBMAP_HUGE_1GB mmap() 标志之一的较大页面大小的各种组合。如果您随机访问数据,您可能会看到 TLB 未命中对性能造成影响,较大的页面大小应该可以解决此问题。我还会检查您的 malloc() 是否使用更大的页面尺寸。
  • madvise(MADV_RANDOM) 可能会有所帮助。
  • 能否请您使用性能分析这两个版本,所以我们至少得到一些提示...

标签: c++ linux performance memory mmap


【解决方案1】:

malloc() 后端可以使用 THP(透明大页面),这是使用文件支持的mmap() 时无法实现的。

使用大页面(甚至是透明的)可以大大减少运行应用程序时的 TLB 未命中次数。

一个有趣的测试可能是禁用透明大页面并再次运行malloc() 测试。 echo never &gt; /sys/kernel/mm/transparent_hugepage/enabled

您还可以使用perf 测量 TLB 未命中:

perf stat -e dTLB-load-misses,iTLB-load-misses ./command

有关 THP 的更多信息,请参阅: https://www.kernel.org/doc/Documentation/vm/transhuge.txt

人们一直在等待具有大页面功能的页面缓存,允许使用大页面(或大页面和标准 4K 页面的混合)映射文件。 LWN上有很多关于透明大页缓存的文章,但还没有进入生产内核。

页面缓存中的透明大页面(2016 年 5 月): https://lwn.net/Articles/686690

今年 1 月还有一个关于 Linux 页面缓存未来的演讲: https://youtube.com/watch?v=xxWaa-lPR-8

此外,您可以通过使用MAP_LOCKED 标志来避免在您的mmap() 实现中对单个页面的所有这些调用mlock。 如果你没有特权,这可能需要调整 memlock 限制。

【讨论】:

  • 非常感谢这个想法。我花了很长时间尝试相反的方法(尝试使用hugetlbfs在安装上创建.bin文件并测量它)。但我无法让它工作。你的想法要简单得多!现在我得到几乎相同的符文时间数字,禁用 THP,mmap 仍然有点慢,但不是很大。您是否有任何有关大页面缓存的更多信息的链接?我有其他机器可以尝试实验内核。
  • 页面缓存中的透明大页面(2016 年 5 月):lwn.net/Articles/686690 好像今年 1 月澳大利亚也有关于 Linux 页面缓存未来的演讲:youtube.com/watch?v=xxWaa-lPR-8 但到目前为止没有什么可以解决你的问题。
  • @Brutos:K. Shutemov(在 Intel)设置了一个补丁到 add huge page support for ext4 backed files,补丁集 v6 于 2017 年 1 月提交。这可能允许您使用 MAP_SHARED | MAP_NORESERVE | MAP_HUGETLB | MAP_HUGE_1GB,假设您启动了具有适当hugepages=hugepagesz= 参数的机器以预先保留这些参数,并使用huge=always 挂载ext4 文件系统。
  • 太糟糕了,现在似乎没有好的解决方案。我会看看我是否可以尽快尝试启动一个自定义编译的内核,看看这个问题是否会在大约 5-7 年内在主流操作系统内核中自行解决。
  • @Morian 非常感谢您的详细回答。
【解决方案2】:

我可能错了,但是...

在我看来,问题不在于mmap,而在于代码将内存映射到文件这一事实。

Linux malloc 回退到 mmap 用于大分配,因此两种内存分配方式本质上使用相同的后端 (mmap)...但是,唯一的区别是 malloc 使用 mmap无需映射到硬盘上的特定文件。

内存信息与磁盘的同步可能是导致“较慢”性能的原因。这类似于几乎不断地保存文件。

您可以考虑在没有文件的情况下测试mmap,方法是使用MAP_ANONYMOUS 标志(以及在某些系统上使用fd == -1)来测试是否有任何差异。

另一方面,我不确定从长远来看,“较慢”的内存访问是否实际上不是更快 - 你会锁定整个事情以将 300Gb 存储到磁盘吗?那需要多长时间? ...

...您以较小的增量自动执行此操作的事实可能是性能提升而不是惩罚。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-12
    • 2019-01-18
    • 1970-01-01
    • 2017-01-08
    • 1970-01-01
    • 2015-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多