Наше программное обеспечение создает структуру данных в памяти, которая составляет около 80 гигабайт. Затем он может либо использовать эту структуру данных непосредственно для выполнения своих вычислений, либо выгружать ее на диск, чтобы впоследствии ее можно было повторно использовать несколько раз. В этой структуре данных происходит много случайных обращений к памяти.
Для большего ввода эта структура данных может расти еще больше (наш самый большой из них был более 300 гигабайт), а наши серверы имеют достаточно памяти для хранения всего в ОЗУ.
Если структура данных сбрасывается на диск, она загружается обратно в адресное пространство с помощью mmap, принудительно вставляется в кеш файл os и, наконец, помещается (код в конце).
Проблема заключается в том, что разница в производительности составляет примерно 16% между простое использование вычисленной структуры данных непосредственно в куче (см. версию Malloc) или mmaping сбрасываемый файл (см. mmap-версию). У меня нет хорошего объяснения, почему это так. Есть ли способ узнать, почему mmap работает намного медленнее? Могу ли я как-то закрыть этот разрыв производительности?
Я сделал измерения на сервере под управлением Scientific Linux 7.2 с ядром 3.10, он имеет 128 ГБ оперативной памяти (достаточно, чтобы соответствовать всем), и повторил их несколько раз с аналогичными результатами. Иногда разрыв немного меньше, но не намного.
Новое обновление (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;
}
Извините С++, его случайный класс проще в использовании. Я скомпилировал его следующим образом:
# 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