почему чтение блоков данных быстрее, чем чтение байта за байтом в файле ввода-вывода
я заметил, что чтение файла byte-by-bye занимает больше времени, чтобы прочитать весь файл, чем чтение файла с помощью fread
.
по данным cplusplus :size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
читает массив count
элементы, каждый из которых имеет размер size
байт из потока и сохраняет их в блоке памяти, указанный ptr
.
Q1) Итак , еще раз fread
читает файл на 1 байт, так что это не так же, как читать 1-байтовым методом ?
Q2) результаты доказали, что еще fread
займет меньше времени .
С здесь:
я запустил это с файлом примерно 44 мегабайт в качестве ввода. При компиляции с VC++2012, я получил следующие результаты:
использование getc Count: 400000 время: 2.034
используя отсчет fread: 400000 время: 0.257
также несколько сообщений на SO говорит об этом, что это зависит от ОС .
Q3) какова роль ОС ?
почему это так и что именно происходит за кулисами ?
5 ответов
fread
тут не чтение файла по одному байту за раз. Интерфейс, который позволяет указать size
и count
отдельно, чисто для вашего удобства. За кулисами, fread
просто читать size * count
байт.
количество байт fread
постараюсь прочитать в после сильно зависит от вашей реализации C и базовой файловой системы. Если вы не знакомы с обоими, часто можно с уверенностью предположить, что fread
будет ближе к оптимальному, чем все, что вы придумаете сами.
изменить: физические диски, как правило, имеют относительно высокое время поиска по сравнению с их пропускной способностью. Другими словами, им требуется относительно много времени, чтобы начать читать. Но после запуска они могут читать последовательные байты относительно быстро. Поэтому без поддержки ОС / файловой системы любой вызов fread
повлечет за собой серьезные издержки для чтения. Таким образом, чтобы эффективно использовать ваш диск, вы захотите прочитать как можно больше байтов еще как возможно. Но диски медленны по сравнению с CPU, RAM и физическими кэшами. Чтение слишком много сразу означает, что ваша программа тратит много времени на ожидание завершения чтения диска, когда он мог бы сделать что-то полезное (например, обработка уже прочитанных байтов).
здесь входит ОС / файловая система. Умные люди, которые работают над ними, потратили много времени, выясняя правильное количество байтов для запроса с диска. Поэтому, когда вы звоните fread
и запрос X
байт, ОС / файловая система переведет это на N
запросы Y
байт каждый. Где Y
является некоторым обычно оптимальным значением, которое зависит от большего количества переменных, чем можно упомянуть здесь.
другая роль ОС / файловой системы-это то, что называется "readahead". Основная идея заключается в том, что большинство IO происходит внутри циклов. Поэтому, если программа запрашивает несколько байтов с диска, есть очень хороший шанс, что она запросит следующие байты вскоре после этого. Из-за этого OS / filesystem обычно читает немного больше, чем вы фактически запросили сначала. Опять же, точная сумма зависит от слишком многих переменных, чтобы упомянуть. Но в принципе, это причина, по которой чтение одного байта за раз все еще несколько эффективно (это было бы еще ~10x медленнее без readahead).
в конце концов, лучше всего думать о fread
Как давать некоторые подсказки ОС / файловой системе о том, сколько байтов вы хотите прочитать. Чем точнее эти подсказки (ближе к общее количество байтов, которые вы хотите прочитать), тем лучше ОС / файловая система оптимизирует ввод-вывод диска.
Это зависит от того, как Вы читаете байт в байт. Но есть значительные накладные расходы на каждый вызов fread
(вероятно, ему нужно сделать вызов OS/kernel).
Если вы называете fread
1000 раз для чтения 1000 байт один за другим, то вы платите, что стоимость 1000 раз; если вы вызываете fread
один раз, чтобы прочитать 1000 байт, то вы платите только эту стоимость один раз.
считайте, что физически происходит с диском. Каждый раз, когда вы просите его выполнить чтение, его голова должна стремиться к правильному положению, а затем ждать, пока под ней закрутится правильная часть блюда. Если вы делаете 100 отдельных 1-байтовых считываний, вам нужно сделать это 100 раз (в первом приближении; на самом деле у ОС, вероятно, есть политика кэширования, которая достаточно умна, чтобы понять, что вы пытаетесь сделать, и прочитать вперед). Но если Вы читаете 100 байт одной операции, и эти байты примерно непрерывный на диске, вам нужно сделать все это только один раз.
комментарий Ханса Пассанта о кэшировании тоже прав на деньги, но даже при отсутствии этого эффекта я ожидал бы, что 1 массовая операция чтения будет быстрее, чем многие маленькие.
Protip: используйте профилировщик, чтобы определить наиболее значительные узкие места в реальной проблеме...
Q1) Итак , еще раз
fread
читает файл на 1 байт, так разве это не то же самое, что читать 1-байтовым методом ?
есть ли что-нибудь из руководства, чтобы предположить, что байты можно читать только по одному за раз? Флэш-память, которая становится все более распространенной, обычно требует, чтобы ваша ОС считывала куски как большие как 512KB одновременно. Возможно, ваша ОС выполняет буферизацию для вашей пользы, поэтому вам не нужно проверять всю сумму...
fgetc будьте медленнее при получении блока байтов, чемQ2) результаты доказали, что еще
fread
займет меньше времени .
fread
. Фактически, оптимальный компилятор может очень хорошо производить тот же машинный код после оптимизации разборы.
на самом деле, это также оказывается недействительным. Большинство доказательств (например, те, которые вы цитируете) пренебрегают рассмотрением влияния, которое setvbuf
(или stream.rdbuf()->pubsetbuf
в C++) есть.
эмпирические данные ниже, однако, интегрирует setvbuf
и, по крайней мере, на каждой реализации, которую я тестировал, показал fgetc
примерно так же быстро, как fread
при чтении большого блока данных, в пределах некоторого бессмысленного поля ошибки, который качается в любом случае... Пожалуйста, выполните эти тесты несколько раз и дайте мне знать, если вы найдете систему, где одна из них значительно быстрее, чем другие. Подозреваю, что нет. Из этого кода можно построить две программы:
gcc -o fread_version -std=c99 file.c
gcc -o fgetc_version -std=c99 -DUSE_FGETC file.c
после компиляции обеих программ создайте test_file
содержащий большое количество байтов, и вы можете проверить так:
time cat test_file | fread_version
time cat test_file | fgetc_version
без дальнейших прощаний, вот код:
#include <assert.h>
#include <stdio.h>
int main(void) {
unsigned int criteria[2] = { 0 };
# ifdef USE_FGETC
int n = setvbuf(stdin, NULL, _IOFBF, 65536);
assert(n == 0);
for (;;) {
int c = fgetc(stdin);
if (c < 0) {
break;
}
criteria[c == 'a']++;
}
# else
char buffer[65536];
for (;;) {
size_t size = fread(buffer, 1, sizeof buffer, stdin);
if (size == 0) {
break;
}
for (size_t x = 0; x < size; x++) {
criteria[buffer[x] == 'a']++;
}
}
# endif
printf("%u %u\n", criteria[0], criteria[1]);
return 0;
}
P. S. Вы, возможно, даже не заметил fgetc
версия проще чем fread
версия; для прохождения символов не требуется вложенный цикл. Это должен быть урок, который нужно убрать здесь: напишите код с учетом обслуживания, а не производительности. При необходимости, вы можете предоставить подсказки (например,setvbuf
) для оптимизации узких мест, которые вы использовали свой профайлер, чтобы определить.
P. P. S. Вы использовали свой профилировщик, чтобы определить это как узкое место в реальной, реальной проблеме, верно?
другими участниками снижения скорости являются перезагрузки конвейера инструкций и утверждения databus. Пропуски кэша данных похожи на перезагрузки конвейера инструкций, поэтому я не представляю их здесь.
вызовы функций и конвейер инструкций
внутренне процессор имеет конвейер команд в кэше (быстрая память физически рядом с процессором). Процессор заполнит конвейер инструкциями, а затем выполнит инструкции и заполнить трубопровод снова. (Обратите внимание, что некоторые процессоры могут получать инструкции по мере открытия слотов в конвейере).
при выполнении вызова функции процессор обнаруживает оператор branch. Процессор не может получить новые инструкции в конвейер, пока ветвь не будет разрешена. Если ветвь выполнена, трубопровод может перезагружаться, теряя время. (Примечание: некоторые процессоры могут читать в достаточном количестве инструкций в кэш, так что нет чтения инструкции необходимы. Примером может служить небольшой цикл.)
в худшем случае, когда вы вызываете функцию чтения 1000 раз, вы вызываете 1000 перезагрузок конвейера инструкций. Если вы вызываете функцию read один раз, конвейер перезагружается только один раз.
Столкновения Databus
Потоки данных через databus от жесткого диска к процессору, потом от процессора к памяти. Некоторые платформы обеспечивают прямой доступ к памяти (DMA)с жесткого диска в память. В любом случае существует конфликт нескольких пользователей с шиной данных.
наиболее эффективным использованием databus является отправка больших блоков данных. Когда пользователь (компонент, такой как процессор или DMA) хочет использовать databus, пользователь должен ждать, пока он станет доступным. В худшем случае другой пользователь отправляет большие блоки, поэтому существует длительная задержка. При отправке 1000 байт, по одному, пользователь должен ждать 1000 раз для других пользователей отказаться от времени с шине.
изображение ждет в очереди (линии) на рынке или в ресторане. Вам нужно приобрести много предметов, но вы покупаете один, а затем должны вернуться и снова ждать в очереди. Или вы могли бы быть как другие покупатели и купить много предметов. Что отнимает больше времени?
резюме
Есть много причин использовать большие блоки для передачи ввода-вывода. Некоторые из причин с физическим приводом, другие включают инструкцию конвейеры, кэши данных и конфликты баз данных. За счет уменьшения количества запросов данных и увеличения размера данных также сокращается время накопления. Один запрос имеет намного меньше накладных расходов, чем 1000 запросов. Если накладные расходы составляют 1 миллисекунду, один запрос занимает 1 миллисекунду, а 1000 запросов-1 секунду.