Почему быстрее читать файл без разрывов строк?

В Python 3.6 требуется больше времени для чтения файла, если есть разрывы строк. Если у меня есть два файла: один с разрывами строк и один без разрывов строк (но в противном случае они имеют один и тот же текст), тогда файл с разрывами строк займет около 100-200% времени для чтения. Я привел конкретный пример.

Шаг # 1: создайте файлы

sizeMB = 128
sizeKB = 1024 * sizeMB

with open(r'C:\temp\bigfile_one_line.txt', 'w') as f:
    for i in range(sizeKB):
        f.write('Hello World!\t'*73)  # There are roughly 73 phrases in one KB

with open(r'C:\temp\bigfile_newlines.txt', 'w') as f:
    for i in range(sizeKB):  
        f.write('Hello World!\n'*73)

Шаг # 2: Прочитайте файл с одной строкой и временем

IPython

%%timeit
with open(r'C:\temp\bigfile_one_line.txt', 'r') as f:
    text = f.read()

Выход

1 loop, best of 3: 368 ms per loop

Шаг №3: Прочитайте файл со многими строками и временем

IPython

%%timeit
with open(r'C:\temp\bigfile_newlines.txt', 'r') as f:
    text = f.read()

Выход

1 loop, best of 3: 589 ms per loop

Это только один пример. Я тестировал это для разных ситуаций, и они делают то же самое:

  • Различные размеры файлов от 1 МБ до 2 ГБ.
  • Использование file.readlines() вместо file.read()
  • Использование пробела вместо закладки ('\ t') в файле с одной строкой (т.е. "Hello World!" )

Я пришел к выводу, что файлы с новыми строками ('\n') занимают больше времени, чем файлы без них. Однако я ожидал бы, что к всем персонажам будут относиться одинаково. Это может иметь важные последствия для производительности при чтении большого количества файлов. Кто-нибудь знает, почему это происходит?

Я использую Python 3.6.1, Anaconda 4.3.24 и Windows 10.

Ответ 1

Когда вы открываете файл в Python в текстовом режиме (по умолчанию), он использует то, что он называет "универсальными новостями" (представлен с PEP 278, но несколько позже изменился с выпуском Python 3). Что универсальные символы новой строки означают, что независимо от того, какие символы новой строки используются в файле, вы увидите только \n в Python. Таким образом, файл, содержащий foo\nbar, будет выглядеть так же, как файл, содержащий foo\r\nbar или foo\rbar (поскольку \n, \r\n и \r - это все соглашения о завершении строки, используемые в некоторых операционных системах за какое-то время).

Логика, которая обеспечивает эту поддержку, вероятно, является причиной ваших различий в производительности. Даже если символы \n в файле не преобразуются, код должен изучить их более тщательно, чем символы, отличные от новой строки.

Я подозреваю, что разница в производительности, которую вы видите, исчезнет, ​​если вы откроете свои файлы в двоичном режиме, где такая поддержка новой строки не предоставляется. Вы также можете передать параметр newline в open в Python 3, который может иметь различные значения в зависимости от того, какое значение вы даете. Я не знаю, какое влияние может оказать какое-то конкретное значение на производительность, но, возможно, стоит проверить, действительно ли разница в производительности, которую вы видите, имеет значение для вашей программы. Я бы попробовал пропустить newline="" и newline="\n" (или что-то вроде того, что заканчивается обычная строка вашей платформы).

Ответ 2

Однако, я ожидал бы, что все символы будут обработаны одинаково.

Ну, это не так. Разрыв линии является особенным.

Разрывы строк не всегда представлены как \n. Причины - длинная история, относящаяся к ранним дням физических телепринтеров, которые я не буду вдаваться здесь, но где эта история закончилась тем, что Windows использует \r\n, Unix использует \n и классическую Mac OS используется \r.

Если вы откроете файл в текстовом режиме, разрывы строк, используемые файлом, будут переведены на \n, когда вы их прочтете, а \n будет переведен в соглашение об отключении строки ОС при написании. В большинстве языков программирования это обрабатывается на лету кодом уровня ОС и довольно дешево, но Python делает все по-другому.

У Python есть функция универсальные новые строки, где она пытается обрабатывать все соглашения о разрыве строк, независимо от того, на какой ОС вы находитесь. Даже если файл содержит комбинацию разрывов строк \r, \n и \r\n, Python распознает их все и переведет их на \n. Универсальные новые строки включены по умолчанию в Python 3, если вы не настроили конкретное соглашение о завершении строки с аргументом newline на open.

В универсальном режиме новой строки реализация файла должна считывать файл в двоичном режиме, проверять содержимое для \r\n символов и

построить новый строковый объект с переводом строк

если он найдет окончание строк \r или \r\n. Если он находит только \n endings или если он вообще не находит окончаний строки, ему не нужно выполнять передачу или строить новый строковый объект.

Построение новой строки и перевод строк завершено. Читая файл с вкладками, Python не должен выполнять перевод.

Ответ 3

В Windows открытие в текстовом режиме преобразует символы '\n' в '\r\n' при записи, а при чтении - наоборот.

Итак, я немного экспериментировал. Я сейчас на MacOS, поэтому моя "родная" строка заканчивается '\n', поэтому я подготовил аналогичный тест для вашего, за исключением использования не-родных строк Windows:

sizeMB = 128
sizeKB = 1024 * sizeMB

with open(r'bigfile_one_line.txt', 'w') as f:
    for i in range(sizeKB):
        f.write('Hello World!!\t'*73)  # There are roughly 73 phrases in one KB

with open(r'bigfile_newlines.txt', 'w') as f:
    for i in range(sizeKB):
        f.write('Hello World!\r\n'*73)

И результаты:

In [4]: %%timeit
   ...: with open('bigfile_one_line.txt', 'r') as f:
   ...:     text = f.read()
   ...:
1 loop, best of 3: 141 ms per loop

In [5]: %%timeit
   ...: with open('bigfile_newlines.txt', 'r') as f:
   ...:     text = f.read()
   ...:
1 loop, best of 3: 543 ms per loop

In [6]: %%timeit
   ...: with open('bigfile_one_line.txt', 'rb') as f:
   ...:     text = f.read()
   ...:
10 loops, best of 3: 76.1 ms per loop

In [7]: %%timeit
   ...: with open('bigfile_newlines.txt', 'rb') as f:
   ...:     text = f.read()
   ...:
10 loops, best of 3: 77.4 ms per loop

Очень похоже на ваш, и обратите внимание, что разница в производительности исчезает, когда я открываю в двоичном режиме. Хорошо, что, если вместо этого я использую * nix line-endings?

with open(r'bigfile_one_line_nix.txt', 'w') as f:
    for i in range(sizeKB):
        f.write('Hello World!\t'*73)  # There are roughly 73 phrases in one KB

with open(r'bigfile_newlines_nix.txt', 'w') as f:
    for i in range(sizeKB):
        f.write('Hello World!\n'*73)

И результаты с использованием этого нового файла:

In [11]: %%timeit
    ...: with open('bigfile_one_line_nix.txt', 'r') as f:
    ...:     text = f.read()
    ...:
10 loops, best of 3: 144 ms per loop

In [12]: %%timeit
    ...: with open('bigfile_newlines_nix.txt', 'r') as f:
    ...:     text = f.read()
    ...:
10 loops, best of 3: 138 ms per loop

Ага! Разница в производительности исчезает! Так что да, я думаю, что использование не-родных строк-концов влияет на производительность, что имеет смысл, учитывая поведение текстового режима.