随机映射内存访问比堆数据访问慢 16%
Random mmaped memory access up to 16% slower than heap data access
我们的软件在内存中构建了一个大约 80 GB 的数据结构。然后它可以直接使用该数据结构进行计算,或者将其转储到磁盘以便以后可以多次重复使用。许多 运行dom 内存访问发生在这个数据结构中。
对于更大的输入,这个数据结构可以变得更大(我们最大的一个超过 300 GB)并且我们的服务器有足够的内存来容纳 RAM 中的所有内容。
如果数据结构被转储到磁盘,它会使用 mmap 加载回地址 space,强制进入 os 页缓存,最后 mlocked(最后的代码)。
问题在于,仅在堆上立即使用计算数据结构(参见 Malloc 版本)或映射转储文件(参见 mmap 版本)在性能上存在大约 16% 的差异。
我没有很好的解释为什么会这样。有没有办法找出为什么 mmap 这么慢?我能以某种方式缩小os这个性能差距吗?
我在服务器 运行 Scientific Linux 7.2 上进行了测量,内核为 3.10,它有 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;
}
请原谅C++,它的运行dom class更容易使用。我是这样编译的:
# 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
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上有一堆关于transparent huge page cache的文章,但是还没有到生产内核。
页面缓存中的透明大页面(2016 年 5 月):
https://lwn.net/Articles/686690
今年 1 月还有一个关于 Linux 页面缓存的未来的演讲:
https://youtube.com/watch?v=xxWaa-lPR-8
此外,通过使用 MAP_LOCKED
标志,您可以避免在 mmap()
实现中的各个页面上调用所有这些 mlock。
如果您没有特权,这可能需要调整 memlock 限制。
我可能是错的,但是...
在我看来,问题不在于 mmap
,而在于代码将内存映射到文件这一事实。
Linux malloc
回落到 mmap
用于大分配,因此两种内存分配风格本质上使用相同的后端(mmap
)...但是,唯一的区别是 malloc
使用 mmap
而不映射到硬盘驱动器上的特定文件。
内存信息与磁盘的同步可能是导致 "slower" 性能的原因。这类似于几乎不断地保存文件。
您可以考虑在没有文件的情况下测试 mmap
,方法是使用 MAP_ANONYMOUS
标志(以及某些系统上的 fd == -1
)来测试任何差异。
另一方面,我不确定 "slower" 内存访问在长 运行 中是否实际上更快 - 你会吗将整个事情锁定到 300Gb 到磁盘?这需要多长时间? ...
...您以小增量自动执行此操作的事实可能是性能提升而不是惩罚。
我们的软件在内存中构建了一个大约 80 GB 的数据结构。然后它可以直接使用该数据结构进行计算,或者将其转储到磁盘以便以后可以多次重复使用。许多 运行dom 内存访问发生在这个数据结构中。
对于更大的输入,这个数据结构可以变得更大(我们最大的一个超过 300 GB)并且我们的服务器有足够的内存来容纳 RAM 中的所有内容。
如果数据结构被转储到磁盘,它会使用 mmap 加载回地址 space,强制进入 os 页缓存,最后 mlocked(最后的代码)。
问题在于,仅在堆上立即使用计算数据结构(参见 Malloc 版本)或映射转储文件(参见 mmap 版本)在性能上存在大约 16% 的差异。 我没有很好的解释为什么会这样。有没有办法找出为什么 mmap 这么慢?我能以某种方式缩小os这个性能差距吗?
我在服务器 运行 Scientific Linux 7.2 上进行了测量,内核为 3.10,它有 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;
}
请原谅C++,它的运行dom class更容易使用。我是这样编译的:
# 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
malloc()
后端可以使用 THP(透明大页面),这在使用由文件支持的 mmap()
时是不可能的。
使用大页面(甚至透明地)可以显着减少 运行 应用程序时 TLB 未命中的次数。
一个有趣的测试可能是禁用透明大页面,然后 运行 您的 malloc()
再次测试。
echo never > /sys/kernel/mm/transparent_hugepage/enabled
您还可以使用 perf
:
perf stat -e dTLB-load-misses,iTLB-load-misses ./command
有关 THP 的更多信息,请参阅: https://www.kernel.org/doc/Documentation/vm/transhuge.txt
人们等待很长时间才能拥有支持大页面的页面缓存,允许使用大页面(或大页面和标准 4K 页面的混合)映射文件。 LWN上有一堆关于transparent huge page cache的文章,但是还没有到生产内核。
页面缓存中的透明大页面(2016 年 5 月): https://lwn.net/Articles/686690
今年 1 月还有一个关于 Linux 页面缓存的未来的演讲: https://youtube.com/watch?v=xxWaa-lPR-8
此外,通过使用 MAP_LOCKED
标志,您可以避免在 mmap()
实现中的各个页面上调用所有这些 mlock。
如果您没有特权,这可能需要调整 memlock 限制。
我可能是错的,但是...
在我看来,问题不在于 mmap
,而在于代码将内存映射到文件这一事实。
Linux malloc
回落到 mmap
用于大分配,因此两种内存分配风格本质上使用相同的后端(mmap
)...但是,唯一的区别是 malloc
使用 mmap
而不映射到硬盘驱动器上的特定文件。
内存信息与磁盘的同步可能是导致 "slower" 性能的原因。这类似于几乎不断地保存文件。
您可以考虑在没有文件的情况下测试 mmap
,方法是使用 MAP_ANONYMOUS
标志(以及某些系统上的 fd == -1
)来测试任何差异。
另一方面,我不确定 "slower" 内存访问在长 运行 中是否实际上更快 - 你会吗将整个事情锁定到 300Gb 到磁盘?这需要多长时间? ...
...您以小增量自动执行此操作的事实可能是性能提升而不是惩罚。