Ответ 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.
По другим прекрасным причинам, чтобы не "разграбить" ваши файлы, прочитайте "Как искать текст файла для шаблона и заменить его на заданное значение.