随机mmaped内存访问比堆数据访问慢16%

Bru*_*tos 15 c++ linux memory performance mmap

我们的软件在内存中构建一个大约80千兆字节的数据结构.然后,它可以直接使用此数据结构进行计算,或将其转储到磁盘,以便之后可以重复使用几次.在此数据结构中发生了大量随机内存访问.

对于更大的输入,这个数据结构可以变得更大(我们最大的一个超过300千兆字节),我们的服务器有足够的内存来容纳RAM中的所有内容.

如果将数据结构转储到磁盘,则会使用mmap将其加载回地址空间,强制进入os页面缓存,最后进行mlocked(最后的代码).

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

我在运行带有3.10内核的Scientific Linux 7.2的服务器上进行了测量,它具有128GB RAM(足以适应所有内容),并且重复了几次,结果相似.有时差距会小一些,但不会太大.

新更新(2017/05/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;
}
Run Code Online (Sandbox Code Playgroud)

请原谅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
Run Code Online (Sandbox Code Playgroud)

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

./mmap
2030ms

./fread
1350ms

./mmap+madv
2030ms

./fread+madv
1350ms

numactl --cpunodebind=0 ./mmap 
2600 ms

numactl --cpunodebind=0 ./fread 
1500 ms
Run Code Online (Sandbox Code Playgroud)

Mor*_*ian 12

malloc()后端可以使用THP(透明大页面),这在使用mmap()文件支持时是不可能的.

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

一个有趣的测试可能是禁用透明的大页面并malloc()再次运行测试. echo never > /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 = xxx-a-PRPR

此外,您可以mmap()使用该MAP_LOCKED标志来避免在实现中的各个页面上对mlock的所有调用.如果您没有特权,则可能需要调整memlock限制.

  • 页面缓存中的透明大页面(2016年5月):https://lwn.net/Articles/686690/似乎在澳大利亚还有关于今年1月Linux页面缓存未来的演示:https:// www .youtube.com/watch?v = xxWaa-lPR-8但到目前为止没有什么可以解决你的问题. (2认同)