Производительность fwrite() значительно ниже емкости диска

У меня есть динамически выделенный массив struct с 17 миллионами элементов. Чтобы сохранить его на диске, пишу

fwrite(StructList, sizeof(Struct), NumStructs, FilePointer)

На следующем этапе я прочитал его с помощью эквивалентного оператора fread, то есть используя sizeof(Struct) и счетчик NumStructs. Я ожидаю, что результирующий файл будет около 3,5 ГБ (это все x64).

Можно ли передать sizeof(Struct) * NumStructs в качестве размера и 1 в качестве счетчика, чтобы ускорить это? Я почесываю голову, почему операция записи может потребовать минут на быстром компьютере с 32 ГБ оперативной памяти (много кеша записи). Я запускал тесты home

Я видел вопрос здесь, так что я спрашиваю, есть ли какой-нибудь цикл внутри fwrite, который может быть "обманут", чтобы быстрее идти, сообщая об этом напишите 1 элемент размера n * s в отличие от n элементов размера s.

ИЗМЕНИТЬ

Я запускал это дважды в режиме выпуска, и оба раза я сдавался, ожидая. Затем я запускал его в режиме отладки, зная, что обычно операции fwrite занимают больше времени. Точный размер данных, которые должны быть записаны, составляет 4 368 892 928 байт. Во всех трех случаях PerfMon показывает два пакета активности записи на диске примерно на 30 секунд, после чего процессор переходит на 100% от одного ядра. Файл находится в этой точке 73 924 608 байт. У меня есть точки останова по обе стороны от fwrite, поэтому я знаю, что там, где он сидит. Конечно, кажется, что что-то застряло, но я оставлю его на ночь и посмотрю.

ИЗМЕНИТЬ

Оставшись на ночь, и он определенно повис в fwrite, файл никогда не проходил мимо 70 МБ.

Ответ 1

Это определенно проблема с fwrite (я пробовал как VS2012, так и 2010).

Начиная со стандартного проекта на С++, я изменил только параметр, чтобы использовать многобайтовый набор символов, цель x64 и многопоточную версию отладки стандартной библиотеки в статической ссылке.

Следующий код преуспевает (без ошибок для краткости):

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
    FILE *fp;
    long long n;
    unsigned char *data;

    n = 4LL * 1024 * 1024 * 1024 - 1;

    data = (unsigned char *)malloc(n * sizeof(unsigned char));

    fp = fopen("T:\\test.bin", "wb");

    fwrite(data, sizeof(unsigned char), n, fp);

    fclose(fp);
}

В отладочной версии моей машины программа заканчивается примерно через 1 минуту (malloc занимает всего несколько секунд, поэтому это в основном fwrite), потребляя в среднем 30% CPU. PerfMon показывает, что запись происходит целиком в конце - это одна "вспышка" 4 ГБ (кэш записи).

Измените - 1 на a + 1 в назначении n, и вы воспроизведете проблему: мгновенное 100% использование ЦП и ничего не записывается. Через несколько минут размер файла оставался 0 байтами (напомним, что в моем фактическом коде ему удается сбросить 70 МБ или около того).

Это определенно проблема в fwrite, так как следующий код может просто написать файл:

int main()
{
    FILE *fp;
    long long n;
    long long counter = 0;
    long long chunk;
    unsigned char *data;

    n = 4LL * 1024 * 1024 * 1024 + 1;

    data = (unsigned char *)malloc(n * sizeof(unsigned char));

    fp = fopen("T:\\test.bin", "wb");

    while (counter < n)
    {
        chunk = min(n - counter, 100*1000);
        fwrite(data+counter, sizeof(unsigned char), chunk, fp);
        counter += chunk;
    }

    fclose(fp);
}

На моей машине это заняло 45 секунд вместо 1 минуты. Использование ЦП не является постоянным, оно поступает в пакеты, а сообщения IO более распределены, чем в методе "один кусок".

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

Я также тестировал fread для чтения сразу после закрытия файла для записи в случае 4 GB + 1, и он возвращается своевременно - всего несколько секунд (никаких реальных данных здесь, поэтому я не проверял его).

ИЗМЕНИТЬ

Я провел несколько тестов с помощью метода кусочной записи и одного вызова fwrite из файла размером 4 ГБ-1 (наибольший размер, который могут выполнять оба метода). Запуск программы несколько раз (с кодом, таким образом, чтобы файл был открыт, написан с несколькими вызовами fwrite, закрыт, затем снова открыт, написан в одном вызове и закрыт), нет сомнений в том, что метод записи блоков быстрее. В худшем случае он возвращается в 68% случаев, когда требуется один звонок, и в лучшем случае я получил всего 20%.

Ответ 2

Это не проблема с fwrite, но предназначенное (хотя и по общему признанию недоброе) поведение:

Функция fwrite() должна записывать из массива, на который указывает ptr, до nitems элементов, размер которых указан size, потоку, на который указывает поток. Для каждого объекта вызовы size должны выполняться функцией fputc(), принимая значения (по порядку) из массива [...]

Итак, в принципе, используя fwrite правильно, без обмана, вы запрашиваете миллиарды звонков на fputc.
С учетом вышеизложенного, он ясно, как вы должны обманывать, чтобы заставить его работать правильно.