Улучшение производительности чтения файла mmap memcpy - PullRequest
0 голосов
/ 17 октября 2018

У меня есть приложение, которое последовательно читает данные из файла.Некоторые считываются непосредственно из указателя на mmap ed файл, а другие части 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), читает файл 29GB изRAID-массив из обычных дисков.

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

4KB 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

1MB 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_SEQUENTIAL настолько замедлил результат копирования в 1 МБ, что я не дождался его завершения (по крайней мере,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 Ответ

0 голосов
/ 08 ноября 2018

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

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

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

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

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's 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 существуют некоторые предостережения.Поддержка файловой системы может быть неубедительной, а реализация прямого ввода-вывода может быть хитрой.Возможно, вам придется использовать выравниваемый по страницам буфер для чтения данных, и вы не сможете прочитать самую последнюю страницу файла, если она не полная страница.

...