Случайный доступ к памяти MMAP до 16% медленнее, чем доступ к данным кучи
Наше программное обеспечение создает структуру данных в памяти, которая составляет около 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
Ответы
Ответ 1
malloc()
back-end может использовать THP (Transparent Huge Pages), что невозможно при использовании mmap()
, поддерживаемого файлом.
Использование огромных страниц (даже прозрачно) может значительно сократить количество пропусков TLB при запуске приложения.
Интересным тестом может быть отключить прозрачные огромные страницы и снова запустить тест malloc()
.
echo never > /sys/kernel/mm/transparent_hugepage/enabled
Вы также можете измерить пропуски TLB с помощью perf
:
perf stat -e dTLB-load-misses,iTLB-load-misses ./command
Для получения дополнительной информации о THP см.
https://www.kernel.org/doc/Documentation/vm/transhuge.txt
Люди ждут долгое время, чтобы иметь кеш страницы, который обладает огромной способностью к работе, что позволяет отображать файлы с использованием огромных страниц (или сочетание огромных страниц и стандартных страниц 4K).
В LWN есть куча статей о прозрачном огромном кеше страниц, но пока еще не достигло производственного ядра.
Прозрачные огромные страницы в кеше страниц (май 2016 года):
https://lwn.net/Articles/686690
Существует также презентация с января этого года о будущем кеша страницы Linux:
https://youtube.com/watch?v=xxWaa-lPR-8
Кроме того, вы можете избежать всех вызовов на mlock на отдельных страницах в своей реализации mmap()
, используя флаг MAP_LOCKED
.
Если вы не являетесь привилегированным, для этого может потребоваться настройка предела блокировки.
Ответ 2
Возможно, я ошибаюсь, но...
Мне кажется, что проблема не в mmap
, а в том, что код отображает память в файл.
Linux malloc
возвращается к mmap
для больших распределений, поэтому оба атрибута выделения памяти по существу используют один и тот же бэкэнд (mmap
)... однако единственное отличие состоит в том, что malloc
использует mmap
без сопоставления с конкретным файлом на жестком диске.
Синхронизация информации о памяти с диском может быть причиной "более медленной" производительности. Это похоже на сохранение файла почти постоянно.
Вы можете рассмотреть возможность тестирования mmap
без файла, используя флаг MAP_ANONYMOUS
(и fd == -1
в некоторых системах) для проверки любой разницы.
С другой стороны,, я не уверен, что "медленный" доступ к памяти на самом деле не ускоряется в долгосрочной перспективе - вы бы заблокировали все это до шага 300Gb на диск? Как долго это займет?...
... тот факт, что вы делаете это автоматически с небольшими приращениями, может быть скорее усилением производительности, чем штрафом.