Подтвердить что ты не робот

Почему std:: fstreams так медленно?

Я работал над простым парсером, и при профилировании я заметил, что узкое место находится в... файле read! Я извлек очень простой тест, чтобы сравнить производительность fstreams и FILE* при чтении большого объема данных:

#include <stdio.h>
#include <chrono>
#include <fstream>
#include <iostream>
#include <functional>

void measure(const std::string& test, std::function<void()> function)
{
    auto start_time = std::chrono::high_resolution_clock::now();

    function();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start_time);
    std::cout<<test<<" "<<static_cast<double>(duration.count()) * 0.000001<<" ms"<<std::endl;
}

#define BUFFER_SIZE (1024 * 1024 * 1024)

int main(int argc, const char * argv[])
{
    auto buffer = new char[BUFFER_SIZE];
    memset(buffer, 123, BUFFER_SIZE);

    measure("FILE* write", [buffer]()
    {
        FILE* file = fopen("test_file_write", "wb");
        fwrite(buffer, 1, BUFFER_SIZE, file);
        fclose(file);
    });
    measure("FILE* read", [buffer]()
    {
        FILE* file = fopen("test_file_read", "rb");
        fread(buffer, 1, BUFFER_SIZE, file);
        fclose(file);
    });
    measure("fstream write", [buffer]()
    {
        std::ofstream stream("test_stream_write", std::ios::binary);
        stream.write(buffer, BUFFER_SIZE);
    });
    measure("fstream read", [buffer]()
    {
        std::ifstream stream("test_stream_read", std::ios::binary);
        stream.read(buffer, BUFFER_SIZE);
    });

    delete[] buffer;
}

Результаты запуска этого кода на моей машине:

FILE* write 1388.59 ms
FILE* read 1292.51 ms
fstream write 3105.38 ms
fstream read 3319.82 ms

fstream запись/чтение примерно в 2 раза медленнее, чем FILE* write/read! И это при чтении большого блока данных без каких-либо синтаксических разборов или других функций fstreams. Я запускаю код в Mac OS, Intel I7 2.6GHz, 16 ГБ 1600 МГц, SSD-накопитель. Обратите внимание, что при повторном запуске того же кода время FILE* read очень низкое (около 200 мс), вероятно, потому, что файл кэшируется... Вот почему файлы, открытые для чтения, не создаются с использованием кода.

Почему при чтении только блога двоичных данных с использованием fstream происходит настолько медленный по сравнению с FILE*?

EDIT 1: Я обновил код и время. Извините за задержку!

EDIT 2: Я добавил командную строку и новые результаты (очень похожие на предыдущие!)

$ clang++  main.cpp -std=c++11 -stdlib=libc++ -O3
$ ./a.out
FILE* write 1417.9 ms
FILE* read 1292.59 ms
fstream write 3214.02 ms
fstream read 3052.56 ms

Следуя результатам для второго запуска:

$ ./a.out
FILE* write 1428.98 ms
FILE* read 196.902 ms
fstream write 3343.69 ms
fstream read 2285.93 ms

Похоже, файл кэшируется при чтении как для FILE*, так и stream, так как время уменьшается с одинаковой суммой для обоих.

EDIT 3: Я сократил код до этого:

FILE* file = fopen("test_file_write", "wb");
fwrite(buffer, 1, BUFFER_SIZE, file);
fclose(file);

std::ofstream stream("test_stream_write", std::ios::binary);
stream.write(buffer, BUFFER_SIZE);

И начал профайлер. Похоже, что stream проводит много времени в функции xsputn, а фактические вызовы write имеют одинаковую продолжительность (как и должно быть, это одна и та же функция...)

Running    Time     Self       Symbol Name
3266.0ms   66.9%    0,0        std::__1::basic_ostream<char, std::__1::char_traits<char> >::write(char const*, long)
3265.0ms   66.9%    2145,0          std::__1::basic_streambuf<char, std::__1::char_traits<char> >::xsputn(char const*, long)
1120.0ms   22.9%    7,0                 std::__1::basic_filebuf<char, std::__1::char_traits<char> >::overflow(int)
1112.0ms   22.7%    2,0                      fwrite
1127.0ms   23.0%    0,0        fwrite

EDIT 4 По какой-то причине этот вопрос отмечен как дубликат. Я хотел бы указать, что я вообще не использую printf, я использую только std::cout для записи времени. Файлы, используемые в части read, представляют собой выходные данные из части write, скопированные с другим именем, чтобы избежать кеширования

4b9b3361

Ответ 1

Казалось бы, в Linux для этого большого набора данных реализация fwrite намного эффективнее, поскольку она использует write, а не writev.

Я не уверен, ПОЧЕМУ writev работает гораздо медленнее, чем write, но, похоже, это разница. И я не вижу абсолютно никакой причины, почему в этом случае fstream должна использовать эту конструкцию.

Это легко увидеть с помощью strace ./a.out (где a.out - это программа, проверяющая это).

Вывод:

Fstream:

clock_gettime(CLOCK_REALTIME, {1411978373, 114560081}) = 0
open("test", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
writev(3, [{NULL, 0}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1073741824}], 2) = 1073741824
close(3)                                = 0
clock_gettime(CLOCK_REALTIME, {1411978386, 376353883}) = 0
write(1, "fstream write 13261.8 ms\n", 25fstream write 13261.8 ms) = 25

ФАЙЛ *:

clock_gettime(CLOCK_REALTIME, {1411978386, 930326134}) = 0
open("test", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1073741824) = 1073741824
clock_gettime(CLOCK_REALTIME, {1411978388, 584197782}) = 0
write(1, "FILE* write 1653.87 ms\n", 23FILE* write 1653.87 ms) = 23

У меня нет им причудливых дисков SSD, поэтому моя машина будет немного медленнее - или что-то еще медленнее в моем случае.

Как отметил Ян Худек, я неверно истолковываю результаты. Я просто написал это:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <chrono>

void measure(const std::string& test, std::function<void()> function)
{
    auto start_time = std::chrono::high_resolution_clock::now();

    function();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start_time);
    std::cout<<test<<" "<<static_cast<double>(duration.count()) * 0.000001<<" ms"<<std::endl;
}

#define BUFFER_SIZE (1024 * 1024 * 1024)


int main()
{
    auto buffer = new char[BUFFER_SIZE];
    memset(buffer, 0, BUFFER_SIZE);

    measure("writev", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY);
        struct iovec vec[] = 
        {
            { NULL, 0 },
            { (void *)buffer, BUFFER_SIZE }
        };
        writev(fd, vec, sizeof(vec)/sizeof(vec[0]));
        close(fd);
    });

    measure("write", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY);
        write(fd, buffer, BUFFER_SIZE);
        close(fd);
    });
}

Это фактическая реализация fstream, которая делает что-то глупое - возможно, копируя все данные в маленьких кусках, где-то и так или иначе, или что-то в этом роде. Я постараюсь узнать дальше.

И результат почти идентичен для обоих случаев и быстрее, чем варианты fstream и FILE* в вопросе.

Edit:

Казалось бы, на моей машине, прямо сейчас, если вы добавите fclose(file) после записи, для моей системы fstream и FILE* - примерно столько же времени для моей системы, примерно за 13 секунд написать 1 ГБ - с дисковыми накопителями старого стиля, а не с SSD.

Я могу быстрее писать MUCH с помощью этого кода:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <chrono>

void measure(const std::string& test, std::function<void()> function)
{
    auto start_time = std::chrono::high_resolution_clock::now();

    function();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start_time);
    std::cout<<test<<" "<<static_cast<double>(duration.count()) * 0.000001<<" ms"<<std::endl;
}

#define BUFFER_SIZE (1024 * 1024 * 1024)


int main()
{
    auto buffer = new char[BUFFER_SIZE];
    memset(buffer, 0, BUFFER_SIZE);

    measure("writev", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY, 0660);
        struct iovec vec[] = 
        {
            { NULL, 0 },
            { (void *)buffer, BUFFER_SIZE }
        };
        writev(fd, vec, sizeof(vec)/sizeof(vec[0]));
        close(fd);
    });

    measure("write", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY, 0660);
        write(fd, buffer, BUFFER_SIZE);
        close(fd);
    });
}

дает время около 650-900 мс.

Я также могу отредактировать исходную программу, чтобы дать время около 1000 мс для fwrite - просто удалите fclose.

Я также добавил этот метод:

measure("fstream write (new)", [buffer]()
{
    std::ofstream* stream = new std::ofstream("test", std::ios::binary);
    stream->write(buffer, BUFFER_SIZE);
    // Intentionally no delete.
});

а затем здесь требуется около 1000 мс.

Итак, мой вывод состоит в том, что, как-то иногда, закрытие файла делает его flush на диск. В других случаях это не так. Я до сих пор не понимаю, почему...

Ответ 2

Поток каким-то образом разбит на MAC, старая реализация или настройка.

Старая настройка может привести к тому, что ФАЙЛ будет записан в каталоге exe и потоке в каталоге пользователя, это не должно иметь никакого значения, если вы не получили 2 диска или другие разные настройки.

На моей паршивой Vista я получаю Обычный буфер + Uncached:
С++ 201103
ФАЙЛ * написать 4756 мс
FILE * прочитано 5007 мс
fstream написать 5526 мс
fstream читать 5728 мс

Обычный буфер + Кэширование:
С++ 201103
FILE * написать 4747 мс
FILE * читать 454 мс
fstream написать письмо 5490 мс
fstream читать 396 мс

Большой буфер + кеширование:
С++ 201103
5-й запуск:
FILE * write 4760 ms
ФАЙЛ * читать 446 мс
fstream написать письмо 5278 мс
fstream читать 369 мс

Это показывает, что запись FILE быстрее, чем fstream, но медленнее в чтении, чем fstream... но все числа находятся в пределах ~ 10% друг от друга.

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

const int MySize = 1024*1024;
char MrBuf[MySize];
stream.rdbuf()->pubsetbuf(MrBuf, MySize);

Эквивалент для FILE -

const int MySize = 1024*1024;
if (!setvbuf ( file , NULL , _IOFBF , MySize )) 
    DieInDisgrace();

Ответ 3

В отличие от других ответов большая проблема с большими файловыми чтениями возникает из-за буферизации со стандартной библиотекой C. Попробуйте использовать низкоуровневые вызовы read/write в больших кусках (1024 КБ) и увидеть скачок производительности.

Буферизация файлов с помощью библиотеки C полезна для чтения или записи небольших фрагментов данных (меньше размера блока диска).

В Windows я получил почти 3-кратное повышение производительности при буферизации файлов при чтении и записи необработанных видеопотоков.

Я также открыл файл, используя собственные вызовы API (win32) API, и сказал ОС не кэшировать файл, так как это включает в себя еще одну копию.

Ответ 4

TL; DR: Попробуйте добавить это в свой код перед записью:

const size_t bufsize = 256*1024;
char buf[bufsize];
mystream.rdbuf()->pubsetbuf(buf, bufsize);

При работе с большими файлами с fstream убедитесь, что использует буфер потока.

В отличие от этого, отключение буферизации потоков значительно снижает производительность. По крайней мере, реализация MSVC копирует 1 char за раз до filebuf, когда буфер не был установлен (см. streambuf::xsputn()), что может сделать ваше приложение привязанным к процессору, что приведет к снижению скорости ввода-вывода.

NB: вы можете найти полное примерное приложение здесь.