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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

Сохраните это как "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 

Он создает пять файлов увеличивающихся размеров. Файлы 1K легко обрабатываются и очень распространены. Раньше считалось, что файлы размером 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]

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

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

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

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

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

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 замедляется линейно. В 1 Мб мы видим, что что-то влияет на "разрывы" ввода-вывода, которые не влияют на чтение по очереди. И поскольку файлы 1MB очень распространены в наши дни, легко заметить, что они замедлят обработку файлов в течение всей жизни программы, если мы не будем думать заранее. Через пару секунд здесь или там не так много, когда они происходят один раз, но если они происходят несколько раз в минуту, это добавляет к серьезному результату работы к концу года.

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

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

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

Ответ 2

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

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

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

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

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

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

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