Почему мой тест на запись на диске С++ намного медленнее, чем просто копия файла с помощью bash?
Используя нижеприведенную программу, я пытаюсь проверить, как быстро я могу записать на диск с помощью std::ofstream
.
Я делаю около 300 Мбайт/с при записи 1 файла GiB.
Однако простая копия файла с использованием команды cp
не менее чем в два раза быстрее.
Является ли моя программа ударом аппаратного ограничения или может быть выполнена быстрее?
#include <chrono>
#include <iostream>
#include <fstream>
char payload[1000 * 1000]; // 1 MB
void test(int MB)
{
// Configure buffer
char buffer[32 * 1000];
std::ofstream of("test.file");
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
auto start_time = std::chrono::steady_clock::now();
// Write a total of 1 GB
for (auto i = 0; i != MB; ++i)
{
of.write(payload, sizeof(payload));
}
double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count();
double megabytes_per_ns = 1e3 / elapsed_ns;
double megabytes_per_s = 1e9 * megabytes_per_ns;
std::cout << "Payload=" << MB << "MB Speed=" << megabytes_per_s << "MB/s" << std::endl;
}
int main()
{
for (auto i = 1; i <= 10; ++i)
{
test(i * 100);
}
}
Вывод:
Payload=100MB Speed=3792.06MB/s
Payload=200MB Speed=1790.41MB/s
Payload=300MB Speed=1204.66MB/s
Payload=400MB Speed=910.37MB/s
Payload=500MB Speed=722.704MB/s
Payload=600MB Speed=579.914MB/s
Payload=700MB Speed=499.281MB/s
Payload=800MB Speed=462.131MB/s
Payload=900MB Speed=411.414MB/s
Payload=1000MB Speed=364.613MB/s
Update
Я изменил с std::ofstream
на fwrite
:
#include <chrono>
#include <cstdio>
#include <iostream>
char payload[1024 * 1024]; // 1 MiB
void test(int number_of_megabytes)
{
FILE* file = fopen("test.file", "w");
auto start_time = std::chrono::steady_clock::now();
// Write a total of 1 GB
for (auto i = 0; i != number_of_megabytes; ++i)
{
fwrite(payload, 1, sizeof(payload), file );
}
fclose(file); // TODO: RAII
double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count();
double megabytes_per_ns = 1e3 / elapsed_ns;
double megabytes_per_s = 1e9 * megabytes_per_ns;
std::cout << "Size=" << number_of_megabytes << "MiB Duration=" << long(0.5 + 100 * elapsed_ns/1e9)/100.0 << "s Speed=" << megabytes_per_s << "MiB/s" << std::endl;
}
int main()
{
test(256);
test(512);
test(1024);
test(1024);
}
Что улучшает скорость до 668MiB/s для файла 1 GiB:
Size=256MiB Duration=0.4s Speed=2524.66MiB/s
Size=512MiB Duration=0.79s Speed=1262.41MiB/s
Size=1024MiB Duration=1.5s Speed=664.521MiB/s
Size=1024MiB Duration=1.5s Speed=668.85MiB/s
Это так же быстро, как dd
:
time dd if=/dev/zero of=test.file bs=1024 count=0 seek=1048576
real 0m1.539s
user 0m0.001s
sys 0m0.344s
Ответы
Ответ 1
Во-первых, вы на самом деле не измеряете скорость записи на диск, но (частично) скорость записи данных в кэш-память ОС. Чтобы действительно измерить скорость записи на диск, данные должны быть сброшены на диск перед вычислением времени. Без промывки может быть разница в зависимости от размера файла и доступной памяти.
В расчетах тоже что-то не так. Вы не используете значение MB
.
Также убедитесь, что размер буфера равен мощности, или, по крайней мере, кратному размеру страницы диска (4096 байт): char buffer[32 * 1024];
. Вы также можете сделать это и для payload
. (похоже, вы изменили это значение с 1024 на 1000 в редактировании, где вы добавили вычисления).
Не используйте потоки для записи (двоичного) буфера данных на диск, а вместо этого пишите непосредственно в файл, используя FILE*, fopen(), fwrite(), fclose()
. См. этот ответ для примера и некоторых таймингов.
Чтобы скопировать файл: откройте исходный файл только в режиме "только для чтения" и, если возможно, в режиме "только вперед" и используя fread(), fwrite()
:
while fread() from source to buffer
fwrite() buffer to destination file
Это должно дать вам скорость, сравнимую со скоростью копирования файла ОС (возможно, вам захочется проверить некоторые разные размеры буфера).
Это может быть немного быстрее с использованием сопоставления памяти:
open src, create memory mapping over the file
open/create dest, set file size to size of src, create memory mapping over the file
memcpy() src to dest
Для больших файлов следует использовать уменьшенные изображения.
Ответ 2
Ответ 3
Я бы сказал, что это что-то умное внутри CP или файловой системы. Если внутри CP, то может быть, что файл, который вы копируете, имеет в нем много 0 и cp обнаруживает это и пишет sparse версию вашего файла. На странице man для cp говорится: "По умолчанию разреженные файлы SOURCE обнаруживаются с помощью грубой эвристики, и соответствующий файл DEST также разрежен". Это может означать несколько вещей, но один из них заключается в том, что cp может сделать разреженную версию вашего файла, что потребует меньше времени записи на диск.
Если в вашей файловой системе это может быть Deduplication.
Как долгожданный 3-й, он может также быть чем-то вроде вашей операционной системы или прошивки вашего диска, которая преобразует чтение и запись в некоторую специализированную инструкцию, которая не требует такой синхронизации, как требуется вашей программе (использование более низкой шины меньше латентности).
Ответ 4
Вы используете относительно небольшой размер буфера. Маленькие буферы означают больше операций в секунду, что увеличивает накладные расходы. Дисковые системы имеют небольшую задержку до того, как получат запрос на чтение/запись и начнут ее обрабатывать; больший буфер амортизируется, что стоит немного лучше. Меньший буфер также может означать, что диск тратит больше времени на поиск.
Вы не выдаете несколько одновременных запросов - вам нужно, чтобы одно чтение заканчивалось до следующего запуска. Это означает, что на диске может быть мертвое время, когда он ничего не делает. Поскольку все записи зависят от всех чтений, а ваши чтения являются серийными, вы голодаете на диск-систему запросов на чтение (вдвойне, так как записи будут отниматься от чтения).
Общее количество запрошенных байтов чтения во всех запросах чтения должно быть больше, чем продукт задержки полосы пропускания для дисковой системы. Если диск имеет задержку в 0,5 мс и производительность 4 ГБ/с, то вы хотите, чтобы все время читалось 4 ГБ * 0,5 мс = 2 МБ.
Вы не используете какие-либо подсказки операционной системы, которые вы выполняете последовательно.
Чтобы исправить это:
- Измените свой код, чтобы иметь еще один выдающийся запрос на чтение в любое время.
- У вас будет достаточно запросов на чтение, чтобы вы ожидали не менее 2 МБ данных.
- Используйте флаги posix_fadvise(), чтобы оптимизировать расписание диска ОС и кеш страниц.
- Рассмотрите возможность использования mmap для сокращения накладных расходов.
- Используйте большой размер буфера для каждого запроса на чтение, чтобы сократить накладные расходы.
Этот ответ содержит более подробную информацию:
fooobar.com/questions/365373/...
Ответ 5
Проблема заключается в том, что вы указываете слишком маленький буфер для вашего fstream
char buffer[32 * 1000];
std::ofstream of("test.file");
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
Ваше приложение запускается в пользовательском режиме. Чтобы записать на диск, вызывается система вызовов write, которая выполняется в режиме ядра. Затем write передает данные в системный кеш, затем в кэш HDD, а затем записывается на диск.
Этот размер буфера влияет на число системных вызовов (1 вызов для каждых 32 * 1000 байтов). Во время системного вызова ОС должна переключать контекст выполнения из пользовательского режима в режим ядра, а затем обратно. Контекст переключения - накладные расходы. В Linux это эквивалентно примерно 2500-3500 простых команд CPU. Из-за этого ваше приложение тратит самое большое время процессора при переключении контекста.
В своем втором приложении вы используете
FILE* file = fopen("test.file", "w");
ФАЙЛ с использованием большего буфера по умолчанию, поэтому он создает более эффективный код. Вы можете попробовать указать небольшой буфер с помощью setvbuf. В этом случае вы должны увидеть такое же ухудшение производительности.
Обратите внимание, что в вашем случае шея бутылки не является характеристикой жесткого диска. Это переключение контекста
![image]()