Почему "похищение" файла не является хорошей практикой?

Почему "прихлебывание" файла не является хорошей практикой для обычного ввода-вывода текстового файла и когда это полезно?

Например, почему я не должен использовать их?

File.read('/path/to/text.txt').lines.each do |line|
  # do something with a line
end

или же

File.readlines('/path/to/text.txt').each do |line|
  # do something with a line
end

3 ответа

Решение

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

Документация дляread говорит:

Открывает файл, дополнительно ищет указанное смещение, затем возвращает длину в байтах (по умолчанию для остальной части файла). [...]

Документация дляreadlines говорит:

Читает весь файл, указанный именем как отдельные строки, и возвращает эти строки в массиве. [...]

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

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

Вот некоторый тестовый код, чтобы продемонстрировать проблему, вызванную "slurping".

Сохраните это как "test.sh":

echo Building test files...

yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000       > kb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000    > mb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000000 > gb1.txt
cat gb1.txt gb1.txt > gb2.txt
cat gb1.txt gb2.txt > gb3.txt

echo Testing...

ruby -v

echo
for i in kb.txt mb.txt gb1.txt gb2.txt gb3.txt
do
  echo
  echo "Running: time ruby readlines.rb $i"
  time ruby readlines.rb $i
  echo '---------------------------------------'
  echo "Running: time ruby foreach.rb $i"
  time ruby foreach.rb $i
  echo
done

rm [km]b.txt gb[123].txt 

Создает пять файлов увеличивающихся размеров. Файлы 1К легко обрабатываются и встречаются очень часто. Раньше считалось, что файлы размером 1 МБ считались большими, но теперь они широко распространены. 1 ГБ является обычным явлением в моей среде, и файлы за пределами 10 ГБ встречаются периодически, поэтому очень важно знать, что происходит при 1 ГБ и более.

Сохраните это как "readlines.rb". Он ничего не делает, кроме внутреннего чтения всего файла построчно и добавления его в массив, который затем возвращается, и кажется, что это будет быстро, так как все написано на C:

lines = File.readlines(ARGV.shift).size
puts "#{ lines } lines read"

Сохраните это как "foreach.rb":

lines = 0
File.foreach(ARGV.shift) { |l| lines += 1 }
puts "#{ lines } lines read"

Бег sh ./test.sh на моем ноутбуке я получаю:

Building test files...
Testing...
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]

Чтение файла 1К:

Running: time ruby readlines.rb kb.txt
28 lines read

real    0m0.998s
user    0m0.386s
sys 0m0.594s
---------------------------------------
Running: time ruby foreach.rb kb.txt
28 lines read

real    0m1.019s
user    0m0.395s
sys 0m0.616s

Чтение 1 МБ файла:

Running: time ruby readlines.rb mb.txt
27028 lines read

real    0m1.021s
user    0m0.398s
sys 0m0.611s
---------------------------------------
Running: time ruby foreach.rb mb.txt
27028 lines read

real    0m0.990s
user    0m0.391s
sys 0m0.591s

Чтение 1 ГБ файла:

Running: time ruby readlines.rb gb1.txt
27027028 lines read

real    0m19.407s
user    0m17.134s
sys 0m2.262s
---------------------------------------
Running: time ruby foreach.rb gb1.txt
27027028 lines read

real    0m10.378s
user    0m9.472s
sys 0m0.898s

Чтение файла 2 ГБ:

Running: time ruby readlines.rb gb2.txt
54054055 lines read

real    0m58.904s
user    0m54.718s
sys 0m4.029s
---------------------------------------
Running: time ruby foreach.rb gb2.txt
54054055 lines read

real    0m19.992s
user    0m18.765s
sys 0m1.194s

Чтение файла 3GB:

Running: time ruby readlines.rb gb3.txt
81081082 lines read

real    2m7.260s
user    1m57.410s
sys 0m7.007s
---------------------------------------
Running: time ruby foreach.rb gb3.txt
81081082 lines read

real    0m33.116s
user    0m30.790s
sys 0m2.134s

Обратите внимание, как readlines работает в два раза медленнее каждый раз, когда размер файла увеличивается, и используя foreach замедляется линейно. На 1MB мы можем видеть, что что-то влияет на "блюкающий" ввод / вывод, что не влияет на построчное чтение. И, поскольку файлы размером 1 МБ очень распространены в наши дни, легко увидеть, что они замедлят обработку файлов в течение всего жизненного цикла программы, если мы не будем забегать вперед. Пара секунд здесь или немного, когда они случаются один раз, но если они происходят несколько раз в минуту, это к концу года приведет к серьезному снижению производительности.

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

иногда "полезно" прихлебывать файл, особенно если вам нужно что-то сделать за границами строки, однако, стоит потратить некоторое время на обдумывание альтернативных способов чтения файла, если вам нужно это сделать. Например, попробуйте сохранить небольшой буфер, созданный из последних "n" строк, и отсканируйте его. Это позволит избежать проблем с управлением памятью, вызванных попыткой чтения и хранения всего файла. Это обсуждается в связанном с Perl блоге " Perl Slurp-Eaze", который охватывает "когда" и "почему", чтобы оправдать использование полных операций чтения файлов, и хорошо относится к Ruby.

По другим уважительным причинам, чтобы не "прихлебывать" ваши файлы, прочитайте " Как найти в тексте файла шаблон и заменить его заданным значением".

Это немного по-старому, но я немного удивлен тем, что никто не упоминает, что прихлебывание входного файла делает программу практически бесполезной для конвейеров. В конвейере входной файл может быть небольшим, но медленным. Если ваша программа работает с ошибками, это означает, что она не работает с данными, когда они становятся доступными, и скорее заставляет вас ждать, сколько бы времени не потребовалось для завершения ввода. Сколько? Это может быть что угодно, например часы или дни, более или менее, если я делаю grep или же find в большой иерархии. Он также может быть разработан, чтобы не завершить, как бесконечный файл. Например, journalctl -f будет продолжать выводить любые события, происходящие в системе, без остановки; tshark будет выводить все, что видит в сети без остановки; ping продолжит пинговать без остановки. /dev/zero бесконечен, /dev/urandom бесконечен.

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

Почему "прихлебывание" файла не является хорошей практикой для обычного ввода-вывода из текстового файла

Оловянный человечек попал в цель. Я также хотел бы добавить:

  • Во многих случаях чтение всего файла в память не поддается отслеживанию (потому что либо файл слишком велик, либо манипуляции со строками имеют экспоненциальное пространство O())

  • Часто вы не можете предвидеть размер файла (особый случай выше)

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

Эта концепция применима не только для файлов, но и для любого другого процесса, в котором объем памяти быстро увеличивается, и вам приходится обрабатывать каждую итерацию (или строку) одновременно. Функции генератора помогают вам, обрабатывая процесс, или чтение строки, одну за другой, чтобы не работать со всеми данными в памяти.

Помимо всего прочего, Python очень умен для чтения файлов и open() Метод предназначен для чтения построчно по умолчанию. См. " Улучшение вашего Python:" доходность "и объяснение генераторов", где объясняется хороший пример использования функций генератора.

Другие вопросы по тегам