Улучшение производительности чтения файлов mmap memcpy

У меня есть приложение, которое последовательно считывает данные из файла. Некоторые считываются непосредственно из указателя на mmap файл эды и другие части memcpy ред из файла в другой буфер. Я заметил низкую производительность при выполнении большой memcpy всей необходимой мне памяти (блоки 1 МБ) и лучшей производительности при выполнении множества меньших вызовов memcpy (в моих тестах я использовал 4 КБ, размер страницы, который занимал 1/3 время для запуска.) Я считаю, что проблема заключается в очень большом количестве серьезных ошибок страницы при использовании большой memcpy.

Я пробовал различные параметры настройки (MAP_POPUATE, MADV_WILLNEED, MADV_SEQUENTIAL) без какого-либо заметного улучшения.

Я не уверен, почему многие маленькие вызовы memcpy должны быть быстрее; это кажется противоречивым. Есть ли способ улучшить это?

Ниже приведены результаты и тестовый код.

Запуск на CentOS 7 (linux 3.10.0), компилятор по умолчанию (gcc 4.8.5), чтение 29-гигабайтного файла из массива RAID обычных дисков.

Работа с /usr/bin/time -v:

4 КБ memcpy:

User time (seconds): 5.43
System time (seconds): 10.18
Percent of CPU this job got: 75%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:20.59
Major (requiring I/O) page faults: 4607
Minor (reclaiming a frame) page faults: 7603470
Voluntary context switches: 61840
Involuntary context switches: 59

1 memcpy:

User time (seconds): 6.75
System time (seconds): 8.39
Percent of CPU this job got: 23%
Elapsed (wall clock) time (h:mm:ss or m:ss): 1:03.71
Major (requiring I/O) page faults: 302965
Minor (reclaiming a frame) page faults: 7305366
Voluntary context switches: 302975
Involuntary context switches: 96

MADV_WILLNEED, похоже, не оказало большого влияния на результат копирования 1 MADV_WILLNEED.

MADV_SEQUENTIAL значительно замедлил результат копирования 1 MADV_SEQUENTIAL, я не дождался его завершения (не менее 7 минут).

MAP_POPULATE замедлил результат копирования 1 Мб примерно на 15 секунд.

Упрощенный код, используемый для теста:

#include <algorithm>
#include <iostream>
#include <stdexcept>

#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
  try {
    char *filename = argv[1];

    int fd = open(filename, O_RDONLY);
    if (fd == -1) {
      throw std::runtime_error("Failed open()");
    }

    off_t file_length = lseek(fd, 0, SEEK_END);
    if (file_length == (off_t)-1) {
      throw std::runtime_error("Failed lseek()");
    }

    int mmap_flags = MAP_PRIVATE;
#ifdef WITH_MAP_POPULATE
    mmap_flags |= MAP_POPULATE;  // Small performance degredation if enabled
#endif

    void *map = mmap(NULL, file_length, PROT_READ, mmap_flags, fd, 0);
    if (map == MAP_FAILED) {
      throw std::runtime_error("Failed mmap()");
    }

#ifdef WITH_MADV_WILLNEED
    madvise(map, file_length, MADV_WILLNEED);    // No difference in performance if enabled
#endif

#ifdef WITH_MADV_SEQUENTIAL
    madvise(map, file_length, MADV_SEQUENTIAL);  // Massive performance degredation if enabled
#endif

    const uint8_t *file_map_i = static_cast<const uint8_t *>(map);
    const uint8_t *file_map_end = file_map_i + file_length;

    size_t memcpy_size = MEMCPY_SIZE;

    uint8_t *buffer = new uint8_t[memcpy_size];

    while (file_map_i != file_map_end) {
      size_t this_memcpy_size = std::min(memcpy_size, static_cast<std::size_t>(file_map_end - file_map_i));
      memcpy(buffer, file_map_i, this_memcpy_size);
      file_map_i += this_memcpy_size;
    }
  }
  catch (const std::exception &e) {
    std::cerr << "Caught exception: " << e.what() << std::endl;
  }

  return 0;
}

Ответ 1

Если базовые файловые и дисковые системы недостаточно быстры, независимо от того, будет ли ваше использование mmap() или POSIX open()/read() или стандартным C fopen()/fread() или C++ iostream совсем неважно,

Если производительность действительно имеет значение, и основная файловая система и дисковая система достаточно mmap(), хотя, mmap(), вероятно, самый худший способ прочитать файл последовательно. Создание сопоставленных страниц является относительно дорогостоящей операцией, и поскольку каждый байт данных считывается только один раз, что стоимость фактического доступа может быть предельной. Использование mmap() также может увеличить давление памяти в вашей системе. Вы можете явно munmap() страницы после их чтения, но тогда ваша обработка может остановиться, пока сопоставления будут снесены.

Использование прямого ввода-вывода, вероятно, будет самым быстрым, особенно для больших файлов, так как не существует большого количества ошибок страниц. Direct IO обходит кеш страниц, что хорошо для чтения данных только один раз. Данные кэширования читаются только один раз - никогда не перечитывать - это не только бесполезно, но и потенциально контрпродуктивно, так как циклы процессоров используются для высылки полезных данных из кеша страниц.

Пример (заголовки и проверка ошибок опущены для ясности):

int main( int argc, char **argv )
{
    // vary this to find optimal size
    // (must be a multiple of page size)
    size_t copy_size = 1024UL * 1024UL;

    // get a page-aligned buffer
    char *buffer;
    ::posix_memalign( &buffer, ( size_t ) ( 4UL * 1024UL ), copy_size );

    // make sure the entire buffer virtual-to-physical mappings
    // are actually done (can actually matter with large buffers and
    // extremely fast IO systems)
    ::memset( buffer, 0, copy_size );

    fd = ::open( argv[ 1 ], O_RDONLY | O_DIRECT );

    for ( ;; )
    {
        ssize_t bytes_read = ::read( fd, buffer, copy_size );
        if ( bytes_read <= 0 )
        {
            break;
        }
    }

    return( 0 );
}

Некоторые предостережения существуют при использовании прямого ввода-вывода в Linux. Поддержка файловой системы может быть пятнистой, а реализация прямого ввода-вывода может быть сложной. Вероятно, вам придется использовать буфер с выравниванием по страницам для чтения данных, и вы не сможете прочитать последнюю страницу файла, если это не полная страница.

Ответ 2

Я думаю, что значительная часть - это использование ЦП. В версии 4KB ваш код вычисляется, что означает, что он будет работать так же быстро, как позволяет процессор. В версии 1 МБ ваш код связан с вводом-выводом, что означает, что он будет работать так же быстро, как позволит подсистема ввода-вывода. Я попытался бы увеличить размер с шагом 4 КБ до тех пор, пока не найду точку пересечения. На данный момент вы, вероятно, увидите свою лучшую производительность. Также обратите внимание: поскольку I/O намного медленнее, чем у процессора, подсистема ввода-вывода обычно имеет кеш, который он использует. Таким образом, хотя вы обрабатываете меньший буфер, подсистема ввода-вывода кэширует следующее чтение. Кроме того, большое чтение может быть принудительно обойти кеш-память вместе.