Случайный доступ к памяти 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_ANONYMOUSfd == -1 в некоторых системах) для проверки любой разницы.

С другой стороны,, я не уверен, что "медленный" доступ к памяти на самом деле не ускоряется в долгосрочной перспективе - вы бы заблокировали все это до шага 300Gb на диск? Как долго это займет?...

... тот факт, что вы делаете это автоматически с небольшими приращениями, может быть скорее усилением производительности, чем штрафом.