Pull to refresh

Comments 34

Как так?

 if (!file)
    {
        file.release();  // тут же моментом выскочит null pointer exception коли уж file == nullptr (проверка на !file успешна)
        return 1;
    }
подмените сэмпл на
int main()
{
    std::unique_ptr<FILE, int(*)(FILE*)> file(nullptr, fclose);
    if (!file)
    {
        file.release();
        return 1;
    }

    return 0;
}

и посмотрите как он отработает.
ru.cppreference.com/w/cpp/memory/unique_ptr/release — вернётся nullptr в никуда и что? Вообще std::unique_ptr::release() noexcept метод
Моя недоглядел. Сыграло роль абсолютное незнание мной концепции «умных указателей». Я думал там нормальный указатель сравнивается с нулём, а чтобы получить нормальный указатель оказывается надо .get() сделать, «умный» то возвращается всегда, ещё и с переопределённым оператором '!'.
Собственно вопросы всё равно остались:
1. Зачем так сложно? Зачем там unique_ptr?
2. Почему в случае ошибки перед завершением программы делается file.release(), а при нормальном завершении — нет?

Это идиоматическое применение unique_ptr в качестве RAII-обёрток над легаси/C-примитивами:


  1. При создании unique_ptr<FILE*, ...> указывается кастомный удалитель, который вызывает fclose(file_ptr_) в деструкторе при штатной работе программы (или при генерации любого исключения).
  2. В случае ошибки fopen по стандарту возвращает nullptr, поэтому в таком случае нет необходимости вызывать fclose. Deamhan, скорее всего, здесь release вообще не нужен, потому что deleter unique_ptr'а и так не вызовется над nullptr.
Да, Вы правы — по стандарту на пустом unique_ptr-е deleter не вызывается. Уже убрал избыточный release(). Вы слегка ошиблись в прототипе — std::unique_ptr<FILE, ...>, иначе придётся FILE** в конструктор передать.

Вау! Не знал, что с помощью умных указателей так легко оборачивать С-шные ресурсы в RAII. Я всегда для этого классы городил. Спасибо!

Не за что. Вообще не думал, что малозначимые первые строки теста привлекут столько внимания. В принципе, можно и виндовые хэндлы так оборачивать через std::remove_pointer_t, правда выглядит это куда более «грязно».
Спасибо за исследование, это вполне годный вариант забесплатно получить ускорение, там где нет возможности что то кардинально поменять.
Но побайтное чтение/запись в stdio все равно слишком медленные и лучше менять алгоритм что бы было линейное чтение/запись большими кусками.
Хотел спросить как вы замеряли скорости что бы избежать побочных эффектов от кеширования файла в памяти после первого прогона, но судя по низким скоростям это не имеет большого смысла в данном случае.
Обычно кэширование содержимого позволяет быстро прочесть файл (например втихую замапив его в память), а тут шёл тест именно записи. Перед каждым файлом содержимое файла сбрасывается в 0, про какой кэш между запусками идёт речь? Или я Вас не так понял?
Про запись я вас случайно запутал. Да, я именно чтение имел в виду, мне одно время нужно было достоверно измерить разницу между разными вариантами чтения и кеш все портил.
Но у вас все равно получается, что измеряется время когда вы отдали файл в кеш ОС, а не когда он по факту был записан на диск.
В любом случае переделывание алгоритма на последовательную запись большими кусками позволит ускориться в несколько раз, потому что даже для HDD на 7200 rpm скорость линейной записи обычно > 200 МБ/с.
Но для бинарных патчей это наверное не актуально :)
Это уже чуть другая вещь, асинхронная, которая происходит прозрачно для алгоритма и её не так просто оценить (современная ОС штука очень хитрая, особенно если памяти много), да оно по сути и не нужно (в данном случае file.reset() можно затащить под end, но сути оно не поменяет). Запись в алгоритме и так по сути последовательная, решалась проблема огромного оверхеда при записи малыми порциями — и тут проблемы больше нет, теперь на вводе-выводе времени транится несколько процентов. Круче разве что прикрутить асинхронную запись, но смысла особого в этом пока нет — есть множество более «дорогих» с точки зрения времени фрагментов.
Я наконец добрался до виндовой машины и попробовал этот же пример, но с записью крупными блоками, а не побайтно.
Собственно вот что я имел ввиду:
C:\>write-byte.exe
Time: 16242738

C:\>write-block.exe
Time: 234701

Версия записывающая байт за байтом чудовищно медленная и грузит ядро на 100%. И кэширование записи в ОС не заметно только потому, что бенчмарк пишет в файл медленнее чем идет запись на диск.
В случае записи блоками все заканчивается практически мгновенно и бенчмарк завершается еще до того, как файл физически будет записан на диск.
Поэтому даже реальную скорость записи на диск таким простым тестом не получится узнать.
А как коррелирует оптимальный размер кэша с размером аппаратного буфера самого HDD? Если взять HDD с буфером 32 или 128 Гб, то как изменится форма графика производительности?
Если учесть, что скорости тут порядка обычной линейной записи, то возможно что корреляции почти 0. Думаю тут куда большее влияние оказывают особенности реализации системы ввода-вывода самой ОС. Лично у меня нет большого набора разных винтов, так что наверняка ответить не могу.
Это не кэш, это внутренний буффер в библиотеке. Потом все это попадет в кэш ОС.
А влияния в данном случае не будет, потому что бутылочное горлышко где то в другом месте.
Тут даже близко не подошли к пределу по линейной записи.
И вы точно не напутали с размерностью МБ/ГБ? :)
Ну, малость попутал :) Но это же такая мелочь :D

Так кому верить? «Близко не подошли к пределу линейной записи» или «Скорости тут порядка обычной линейной записи», если верить предыдущему оратору?

Вы еще не отметили, что hdd не только размером буфера отличаются, но и скоростью вращения, 5400, 7200, 10000 rpm точно дадут разную скорость записи. Но в целом, буфер играет роль только в сценариях, когда происходит например копирование неск. гигабайт данных из одной папки в другую, а когда на диск пишутся данные, которые генерирует какая-то программа, размер буфера не должен влиять на скорость записи, т.к. как только он заполнится, мы упремся в сам диск (вращение+головка).

На самом деле эти утверждения не противоречат друг другу: предел линейной записи — это скорость внешней дорожки, внутренняя обычно в пару раз медленнее. Итого в среднем скорость линейного чтения файлов на современном 7200 rpm 3.5" HDD около 150 мб/с, тест скорости кэша в моём случае выдал почти 500 мб/с. Итого: к чему по порядку ближе полученные результаты? Достигнут ли предел по скорости линейной записи?
В нормальных случаях код должен в разы быстрее поставлять данные на запись, чем физическое устройство будет их записывать.
Писать в файл по байту за вызов так себе вариант, чудовищно медленный даже в случае использования буферизующей прослойки в виде stdio.
В данном случае это просто пример как можно за бесплатно ускорить легаси, но тот же самый код пишущий большими блоками справляется с задачей за доли секунды.
Это нужно учитывать на уровне алгоритма.

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

Хотелось по-максимуму сохранить портабельность, не дописывая кейсов для разных ОС, и в целом обойтись минимальными изменениями. Если бы не удалось решить проблему так легко — перешёл бы к маппингу.
Совсем не факт, будет зависеть от разных факторов и нужно сравнивать все равно.
Вот отличный доклад где упоминают почему mmap хуже при простом линейном доступе к файлу:
Ну да, работать через системные вызовы напрямую, используя O_DIRECT, то действительно может быть быстрее. Но libc так не делает, насколько я знаю. Поэтому кеширование данных ядром остается. И докладчик правильно сказал, что намного быстрее замапить этот файловый кеш себе в адресное пространство, чем делать лишнее копирование при использовании read() без O_DIRECT.

Думаю, что mmap() все же будет быстрее fopen()/fread(), но возможно медленнее open(..., O_DIRECT)/read(). В некоторых случаях.
Обычный fwrite из этого примера начинает писать быстрее 2ГБ/с при использовании крупных блоков — бутылочным горлышком становится оборудование, а не способ записи.
От одновременного использования одной FILE структуры из нескольких потоков. По сути это лок/анлок мьютекса
Попробовал на железном линуксе
HDD WDC WD20EZRZ — 5400 rpm буфер 64М.

$ ./fwrite_test.fwrite_nosetbuf
Time: 11718859
$ ./fwrite_test.fwrite_unlock_nosetbuf
Time: 9806416
$ ./fwrite_test.fwrite_unlock_setbuf
Time: 9803208

Разница заметна, но не так как в виртуалке.
прогнал несколько раз, погрешность ~1% картины особенно не меняет.
Здорово, что общая закономерность та же, и похоже накладные расходы на lock/unlock в linux меньше. Вообще в Вашем случае, похоже, скорость ограничена самим накопителем (неудачное расположение файла и т.п.). Что за дистрибутив использовали? Какая версия gcc? Собирали с теми же параметрами, что я указывал? Раз уж зашёл разговор такой, у меня сейчас есть возможность произвести те же замеры на linux десктопе, как соберу данные — отпишусь.
Я «дико извиняюсь». Вообще написал, потому что хотел заметить что виртуалка не лучшее место для проведения тестов и выводов по i/o. поэтому тупо скопировал строку
g++ -o2 -s -static-libgcc -static-libstdc++ fwrite_test.cpp -o fwrite_test
и только после вашего поста задумался — файло-то и правда, 512М а не Г, почему-же так медленно? Попробовал на tmpfs — та-же картина.
Дело в -o2, с -O2 — совсем другое дело. На hdd
$ ./fwrite_test.unlock_nobuf
Time: 4637970
$ ./fwrite_test.unlock_buf
Time: 4636879
$ ./fwrite_test.fwrite
Time: 6129556
Почти то-же самое на ssd/tmpfs.
И еще, вместо fwrite_unlocked(&b, 1, sizeof(b), file.get());
видимо, должно быть fwrite_unlocked(&b, sizeof(b), 1, file.get());

PS: linux gentoo x86_64 gcc 8.2
а еще — какая виртуалка у вас, на vbox/win7 разница получилась менее заметной.
Да, виртуалка не совсем то для тестов, но лучше варианта под рукой тогда не было (виртуалка HyperV из 1809 win 10). Сейчас я дорвался до железной линухи — соберу результаты и заменю таковые от виртуалки. Неточности исправил, спасибо. Отдельное спасибо за результаты теста.
Заменил данные с виртуалки на таковые с реальной машины, в полученных данных та же закономерность, что и в ваших.
Если работать напрямую с диском (на сколько это позволяет ОС, конечно), то последовательная запись/чтение 256КБ по одному сектору будет значительно медленней, чем запись одного пакета в 256КБ. Определяется это тем, что диск — это тоже «компьютер» и у него внутри тоже есть накладные расходы на совершение операций. Есть также и волшебная величина, после которой рост скорости почти не происходит, она определяется максимальным размером буфера, который можно передать накопителю на запись (maximum transfer length). Эту величину можно запросить у накопителя, в том числе через API ОС. Очень популярная величина — 128КБ, но современные накопители могут и больше, например, 2МБ.
Sign up to leave a comment.

Articles