Улучшение/оптимизация скорости записи файлов в C++

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

Чтобы объяснить, моя цель — захватить поток данных, поступающих через гигабитный Ethernet, и просто сохранить его в файл.

Необработанные данные поступают со скоростью 10 МС/с, затем сохраняются в буфере и впоследствии записываются в файл.

Ниже приведен соответствующий раздел кода:

    std::string path = "Stream/raw.dat";
    ofstream outFile(path, ios::out | ios::app| ios::binary);

    if(outFile.is_open())
        cout << "Yes" << endl;

    while(1)
    {
         rxSamples = rxStream->recv(&rxBuffer[0], rxBuffer.size(), metaData);
         switch(metaData.error_code)
         {

             //Irrelevant error checking...

             //Write data to a file
                std::copy(begin(rxBuffer), end(rxBuffer), std::ostream_iterator<complex<float>>(outFile));
         }
    } 

Проблема, с которой я сталкиваюсь, заключается в том, что запись образцов в файл занимает слишком много времени. Примерно через секунду устройство, отправляющее образцы, сообщает, что его буфер переполнен. После некоторого быстрого профилирования кода почти все время выполнения тратится на std::copy(...) (99,96% времени, если быть точным). Если я уберу эту строку, я смогу запускать программу часами, не сталкиваясь с переполнением.

Тем не менее, я довольно озадачен тем, как я могу улучшить скорость записи. Я просмотрел несколько сообщений на этом сайте, и кажется, что наиболее распространенным предложением (в отношении скорости) является реализация записи файлов, как я уже сделал, - с использованием std::copy.

Если это полезно, я запускаю эту программу на Ubuntu x86_64. Мы ценим любые предложения.


person Mlagma    schedule 05.08.2015    source источник
comment
Это про USRP, не так ли?   -  person Marcus Müller    schedule 05.08.2015
comment
Интересно... чистое направление указателя C может помочь вам лучше. Если вы знаете структуру своей операционной системы, возможно, вы сможете быстрее получить доступ к памяти.   -  person A. Abramov    schedule 05.08.2015
comment
Ага... Я использую USRP N210.   -  person Mlagma    schedule 05.08.2015
comment
Копирует ли std::copy элементы? Это распространенная ошибка при выполнении IO. Супер медленно.   -  person usr    schedule 05.08.2015
comment
добавлены теги USRP и программно-определяемого радио, поскольку они применимы здесь. Недостаток общей производительности системы, необходимой для обработки в реальном времени, является очень распространенной проблемой.   -  person Marcus Müller    schedule 05.08.2015
comment
@A.Abramov UHD, интерфейс устройства, который использует Mlagma, - это C ++ (у него есть совершенно новая оболочка C, но она не так практична, как исходный / базовый C ++, а также не быстрее).   -  person Marcus Müller    schedule 05.08.2015
comment
Запись на диск идет медленно. Не ожидайте, что сможете писать более 50 МБ/с. Вы мало что можете сделать, чтобы улучшить эту ситуацию; Рассмотрите возможность сохранения данных на RAM-диск (например, tmpfs) или купите более быстрое запоминающее устройство (например, SSD).   -  person fuz    schedule 05.08.2015
comment
@FUZxxl да, но, пожалуйста, имейте в виду, что не каждый SSD соответствует этой скорости записи - видите ли, вам нужна скорость записи, которую USRP применяет как абсолютный минимум скорости для краткосрочных средних значений, а не как среднее значение по всему диску. . Часто даже твердотельные накопители не справляются с этой задачей. На самом деле было много дискуссий о том, как заставить все работать на скорости 100 мс/с.   -  person Marcus Müller    schedule 05.08.2015
comment
вы, вероятно, захотите немного сжать данные, чтобы в целом вы перемещали меньше байтов.   -  person ratchet freak    schedule 13.03.2018


Ответы (2)


Таким образом, основная проблема здесь заключается в том, что вы пытаетесь писать в том же потоке, что и получаете, а это означает, что ваш recv() может быть вызван снова только после завершения копирования. Несколько наблюдений:

  • Перенесите запись в другую ветку. Речь идет об USRP, поэтому GNU Radio действительно может быть инструментом, который вы выберете — он по своей сути многопоточен.
  • Ваш выходной итератор, вероятно, не самое эффективное решение. Просто «записать ()» в дескриптор файла может быть лучше, но это измерения производительности, которые зависят от вас.
  • Если ваш жесткий диск/файловая система/ОС/ЦП не соответствуют скорости, поступающей от USRP, даже если отделить получение от записи по потоку, то вы ничего не можете сделать - получите более быструю систему.
  • Вместо этого попробуйте записать на RAM-диск

На самом деле, я не знаю, как вы пришли к подходу std::copy. пример rx_samples_to_file, поставляемый с UHD, делает это с помощью простой записи, и вы определенно следует предпочесть это копированию; файловый ввод-вывод в хороших операционных системах часто может выполняться на одну копию меньше, а итерация по всем элементам, вероятно, очень медленная.

person Marcus Müller    schedule 05.08.2015
comment
Согласен, добавлю еще: записывайте входящие данные в один или несколько огромных буферов (в зависимости от времени задержки между получением данных и записью в файл). Создайте поток, который читает из этого буфера и записывает в файл (огромными блоками). Кроме того, используйте как можно больше аппаратной помощи, такой как DMA. - person Thomas Matthews; 05.08.2015
comment
@ThomasMatthews В целом согласен, что большие фрагменты данных = лучшая производительность, но у этого также есть недостатки, а именно, если фрагменты не становятся слишком большими, ОС может не быть занята их слишком долгой обработкой, а также в системах, где ядра ЦП разрежены, время, в течение которого ОС занята файловым вводом-выводом, может стать критическим, если она не может одновременно получать данные по сети. Linux довольно хорошо масштабируется на нескольких ядрах, так что на самом деле это проблема только одноядерных процессоров. - person Marcus Müller; 05.08.2015
comment
@ThomasMatthews Я переключился на write, как вы предложили, и это огромное улучшение - оно еще не переполнилось. Я также увеличил размер своего буфера. - person Mlagma; 05.08.2015
comment
@Mlagma приятно это слышать! Как только ваше приложение делает больше, чем просто запись в файл (что вы могли бы просто сделать с упомянутой программой-примером), я все же рекомендую использовать многопоточность. - person Marcus Müller; 05.08.2015
comment
@ThomasMatthews Сейчас я работаю над многопоточным приложением. На практике я бы не стал писать файл в этой функции. Пока это только для проверки концепции. Недавно я только что закончил работу над многопоточным буфером для передачи сэмплов в разные части программы, такие как демодуляция и тому подобное. - person Mlagma; 05.08.2015
comment
Два потока не нужны, потому что и ввод, и вывод буферизуются операционной системой. Одного потока достаточно, чтобы запустить оба устройства на максимум (до узкого места). - person usr; 06.08.2015
comment
@usr: в идеальном мире это может быть правдой, но наличие пула буферов, обменивающихся данными между принимающим и записывающим оборудованием, здесь абсолютно необходимо, как показали многие примеры использования USRP. - person Marcus Müller; 08.08.2015
comment
@usr: чтобы объяснить немного лучше: если бы буферизация была произвольно гибкой, а пропускная способность была бы единственной важной мерой, ваш комментарий был бы верным. Однако проблема здесь в том, что ни одно из предположений не верно: задержка имеет значение, потому что через некоторое время буферизация пакетов, поступающих через гигабитный Ethernet (или USB3, или 10GigE, или PCIe, чтобы назвать интерфейсы текущих невстроенных USRP) , буферы просто заполнены, и ОС и оборудование вынуждены сбрасывать данные. Это катастрофа! Так что нет, вы не можете полагаться на свою ОС, чтобы угадать, какая архитектура нужна вашему приложению. - person Marcus Müller; 08.08.2015
comment
@MarcusMüllerꕺꕺ, если мы предполагаем, что место назначения потоковой передачи может обрабатывать все данные, то даже небольшой буферизации должно быть достаточно. Достаточно, чтобы чтение и письмо перекрывались. При этом, если это UDP (на что он не похож, а выглядит как TCP), то пользовательская буферизация может иметь смысл, чтобы увеличить вероятность того, что он будет работать. С другой стороны, простого увеличения размера буфера сокета также должно быть достаточно. - person usr; 08.08.2015
comment
@usr: реальный мировой опыт показывает, что это предположение неверно, даже с чередованием рейдов SSD для высоких скоростей. Не поймите меня неправильно, если вы занимаетесь обработкой данных в целом, ваши предположения в порядке, но это мир реального времени, где ПК работают не со средней скоростью, а с кратковременными задержками, которые легко могут стать слишком большим. Кстати, UHD — это UDP (TCP не имеет смысла, пакет, опоздавший на несколько мс, просто бесполезен). ОП уже увеличил размеры буфера. Действительно, ваши предположения не соответствуют действительности, вот. - person Marcus Müller; 08.08.2015

Давайте немного посчитаем.

Ваши образцы (очевидно) относятся к типу std::complex<std::float>. Учитывая (типичное) 32-битное число с плавающей запятой, это означает, что каждая выборка составляет 64 бита. При 10 MS/s это означает, что необработанные данные составляют около 80 мегабайт в секунду — это в пределах того, что вы можете ожидать при записи на жесткий диск настольного компьютера (7200 об/мин), но довольно близко к пределу (который обычно составляет около 100 мегабайт в секунду). -100 мегабайт в секунду или около того).

К сожалению, несмотря на std::ios::binary, вы на самом деле записываете данные в текстовом формате (поскольку std::ostream_iterator в основном выполняет stream << data;).

Это не только снижает точность, но и увеличивает размер данных, по крайней мере, как правило. Точная величина увеличения зависит от данных — небольшое целочисленное значение может фактически уменьшить количество данных, но для произвольного ввода увеличение размера, близкое к 2:1, является довольно распространенным явлением. С увеличением 2:1 ваши исходящие данные теперь составляют около 160 мегабайт в секунду, что быстрее, чем может обработать большинство жестких дисков.

Очевидной отправной точкой для улучшения было бы вместо этого записывать данные в двоичном формате:

uint32_t nItems = std::end(rxBuffer)-std::begin(rxBuffer);
outFile.write((char *)&nItems, sizeof(nItems));
outFile.write((char *)&rxBuffer[0], sizeof(rxBuffer));

На данный момент я использовал sizeof(rxBuffer) в предположении, что это реальный массив. Если на самом деле это указатель или вектор, вам нужно будет вычислить правильный размер (что вам нужно, так это общее количество записываемых байтов).

Я также хотел бы отметить, что на данный момент ваш код имеет еще более серьезную проблему: поскольку он не указал разделитель между элементами при записи данных, данные будут записываться без каких-либо элементов, отделяющих один элемент от следующий. Это означает, что если вы запишете два значения (например) 1 и 0.2, то, что вы прочитаете, будет не 1 и 0.2, а одно значение 10.2. Добавление разделителей к вашему текстовому выводу добавит еще больше накладных расходов (приблизительно на 15% больше данных) к процессу, который уже дает сбой, потому что генерирует слишком много данных.

Запись в двоичном формате означает, что каждое число с плавающей запятой будет потреблять ровно 4 байта, поэтому разделители не нужны для правильного чтения данных.

Следующим шагом после этого будет переход к подпрограмме файлового ввода-вывода более низкого уровня. В зависимости от ситуации это может или не может иметь большого значения. В Windows вы можете указать FILE_FLAG_NO_BUFFERING при открытии файла с помощью CreateFile. Это означает, что операции чтения и записи в этот файл в основном будут выполняться в обход кеша и будут выполняться непосредственно на диск.

В вашем случае это, вероятно, выигрыш - при 10 MS/s вы, вероятно, довольно долго израсходуете пространство кеша, прежде чем перечитаете те же данные. В таком случае размещение данных в кеше практически ничего не даст, но вам придется потратить некоторые данные, чтобы скопировать данные в кеш, а затем несколько позже скопировать их на диск. Хуже того, он может загрязнить кэш всеми этими данными, поэтому он больше не хранит другие данные, которые, скорее всего, выиграют от кэширования.

person Jerry Coffin    schedule 05.08.2015
comment
Не обходить буферизацию. Выполняйте ввод-вывод асинхронно или, по крайней мере, в отдельном потоке. Тратить ЦП плохо, но что бы вы ни делали, держите буфер ОС заполненным, чтобы он мог поддерживать эффективность диска. - person Eliot Gillum; 13.03.2018
comment
@EliotGillum: именно такие комментарии убеждают лучших участников SO в том, что они могли бы с таким же успехом бросить курить и выращивать цветы вместо того, чтобы пытаться помочь другим. У вас много компании, но вы несете личную ответственность за то, чтобы сделать мир хуже. Перечитайте последний абзац ответа. Продолжайте перечитывать его столько раз, сколько необходимо, чтобы понять, что ваш комментарий полностью и совершенно неверен. - person Jerry Coffin; 13.03.2018
comment
жестокость никогда не сделает сообщество лучше, независимо от того, сколько у вас репутации - person Eliot Gillum; 13.03.2018
comment
Я не думаю, что это жестокость. Я думаю, что это наблюдение факта. ... моя цель - захватить поток данных, поступающих через гигабитный Ethernet, и просто сохранить его в файл. Это не показывает никаких указаний на то, что он получит выгоду от загрязнения кеша содержимым файла, который он получает. - person Jerry Coffin; 14.03.2018