Представление времени в создании очень большого текстового файла в Python

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

Seq_num<SPACE>num_val
12343234 759

предположим, я собираюсь создать файл со 100 миллионами строк. Я пробовал 2 подхода, и удивительно, что они дают очень разную производительность времени.

  1. для петли над 100m. В каждом цикле я делаю короткую строку seq_num<SPACE>num_val, а затем я пишу это в файл. Этот подход принимает большое of время.

    ## APPROACH 1  
    for seq_id in seq_ids:
        num_val=rand()
        line=seq_id+' '+num_val
        data_file.write(line)
    
  2. для петли над 100m. В каждом цикле я делаю короткую строку seq_num<SPACE>num_val, а затем я добавляю это в список. Когда цикл заканчивается, я перебираю элементы списка и записываю каждый элемент в файл. Этот подход принимает гораздо меньше времени.

    ## APPROACH 2  
    data_lines=list()
    for seq_id in seq_ids:
        num_val=rand()
        l=seq_id+' '+num_val
        data_lines.append(l)
    for line in data_lines:
        data_file.write(line)
    

внимание:

  • подход 2 имеет 2 цикла вместо 1 цикла.
  • я пишу в файл в цикле для подхода 1 и подхода 2. Так это шаг должен быть одинаковым для обоих.

так подход 1 должен занять меньше времени. Есть намеки на то, что я упускаю?

5 ответов


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

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

они пропустили тот факт, что вы повторяете цикл for для записи файл, который на самом деле не нужен.

вместо этого, увеличивая использование памяти (в этом случае это доступно, так как 100 миллионов линейных файлов будет около 600 МБ), вы можете создать только одну строку более эффективным способом, используя форматирование или функции соединения python str, а затем записать большую строку в файл. Также полагаясь на понимание списка, чтобы получить данные для форматирования.

С loop1 и loop2 ответа @Tombart, я получаю elapsed time 0:00:01.028567 и elapsed time 0:00:01.017042, соответственно.

в то время как с этим кодом:

start = datetime.now()

data_file = open('file.txt', 'w')
data_lines = ( '%i %f\n'%(seq_id, random.random()) 
                            for seq_id in xrange(0, 1000000) )
contents = ''.join(data_lines)
data_file.write(contents) 

end = datetime.now()
print("elapsed time %s" % (end - start))

Я elapsed time 0:00:00.722788 что примерно на 25% быстрее.

обратите внимание, что data_lines является выражением генератора, поэтому список на самом деле не хранится в памяти, а строки генерируются и потребляются по требованию join метод. Это означает, что единственной переменной, которая значительно занимает память, является contents. Это также несколько сокращает время работы.

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

выводы:

  • всегда старайтесь делать понимание списка вместо простого для циклов (понимание списка даже быстрее, чем filter для списков фильтрации посмотреть здесь).
  • если возможно ограничениями памяти или реализации, попробуйте создать и кодировать строку содержание сразу, используя format или join функции.
  • если это возможно, и код остается читаемым, используйте встроенные функции, чтобы избежать for петли. Например, используя extend функция списка вместо итерации и использования append. Фактически, оба предыдущих момента можно рассматривать как примеры этого замечания.

Примечание. Хотя этот ответ можно считать полезным сам по себе, он не полностью решает вопрос, который почему две петли опция в вопросе, кажется, работает быстрее в некоторых средах. Для этого, возможно, ответ @Aiken Drum ниже может пролить свет на этот вопрос.


много и гораздо меньше технически очень расплывчатые термины:) в основном, если вы не можете измерить его, вы не можете его улучшить.

для простоты давайте проведем простой тест, loop1.py:

import random
from datetime import datetime

start = datetime.now()
data_file = open('file.txt', 'w')
for seq_id in range(0, 1000000):
        num_val=random.random()
        line="%i %f\n" % (seq_id, num_val)
        data_file.write(line)

end = datetime.now()
print("elapsed time %s" % (end - start))

loop2.py С 2 петлями:

import random
from datetime import datetime

start = datetime.now()
data_file = open('file.txt', 'w')
data_lines=list()
for seq_id in range(0, 1000000):
    num_val=random.random()
    line="%i %f\n" % (seq_id, num_val)
    data_lines.append(line)
for line in data_lines:
    data_file.write(line)

end = datetime.now()
print("elapsed time %s" % (end - start))

когда я запускаю эти два скрипта на своих компьютерах (с SSD-накопителем) ,я получаю что-то вроде:

$ python3 loop1.py 
elapsed time 0:00:00.684282
$ python3 loop2.py 
elapsed time 0:00:00.766182

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

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


Ниже приведено расширение элегантного ответа @Tombart и несколько дополнительных замечаний.

имея в виду одну цель: оптимизировать процесс чтения данных из цикла (ов), а затем записать его в файл, давайте начнем:

я буду использовать with инструкция для открытия / закрытия файла test.txt во всех случаях. Эта инструкция автоматически закрывает файл при выполнении блока кода внутри него.

еще один важный момент, чтобы рассмотреть путь Python обрабатывает текстовые файлы на основе операционной системы. От docs:

Примечание: Python не зависит от представления базовой операционной системы о текстовых файлах; вся обработка выполняется самим Python и, следовательно, не зависит от платформы.

это означает, что эти результаты могут незначительно отличаться при выполнении в ОС Linux/Mac или Windows. Небольшое изменение может произойти от других процессов используя один и тот же файл в то же время или несколько процессов ввода-вывода, происходящих в файле во время выполнения сценария, общая скорость обработки процессора среди других.

я представляю 3 случая со временем выполнения для каждого и, наконец, найти способ дальнейшей оптимизации наиболее эффективного и быстрого случая:

первый случай: цикл по диапазону (1,1000000) и запись в файл

import time
import random

start_time = time.time()
with open('test.txt' ,'w') as f:
    for seq_id in range(1,1000000):
        num_val = random.random()    
        line = "%i %f\n" %(seq_id, num_val)
        f.write(line)

print('Execution time: %s seconds' % (time.time() - start_time)) 

#Execution time: 2.6448447704315186 seconds

Примечание: в двух list сценарии ниже, у меня есть инициализируется пустой список data_lines как:[] вместо list(). Причина: [] примерно в 3 раза быстрее, чем list(). Вот объяснение такого поведения:--75-->почему [] быстрее, чем list()?. Основная суть дискуссии: While [] создается как код объекты и один инструкция, list() - это отдельный объект Python, который также требует разрешения имен, глобальных вызовов функций и стек должен быть вовлечен, чтобы толкать аргументы.

используя функцию timeit () в модуле timeit, вот сравнение:

import timeit                 import timeit                     
timeit.timeit("[]")           timeit.timeit("list()")
#0.030497061136874608         #0.12418613287039193

второй случай: цикл по диапазону (1,1000000), добавление значений в пустой список, а затем запись в файл

import time
import random

start_time = time.time()
data_lines = []
with open('test.txt' ,'w') as f:
    for seq_id in range(1,1000000):
        num_val = random.random()    
        line = "%i %f\n" %(seq_id, num_val)
        data_lines.append(line)
    for line in data_lines:
        f.write(line)

print('Execution time: %s seconds' % (time.time() - start_time)) 

#Execution time: 2.6988046169281006 seconds

третий случай: цикл над пониманием списка и запись в файл

благодаря мощному и компактному пониманию списка Python можно оптимизировать процесс дальше:

import time
import random

start_time = time.time()

with open('test.txt' ,'w') as f: 
        data_lines = ["%i %f\n" %(seq_id, random.random()) for seq_id in range(1,1000000)]
        for line in data_lines:
            f.write(line)

print('Execution time: %s seconds' % (time.time() - start_time))

#Execution time: 2.464804172515869 seconds

на нескольких итерациях я всегда получал более низкое значение времени выполнения в этом случае по сравнению с предыдущими двумя случаями.

#Iteration 2: Execution time: 2.496004581451416 seconds

теперь возникает вопрос: почему понимание списка (и вообще списков ) быстрее по последовательному for петли?

интересный способ анализа того, что происходит при последовательном for петли выполняются и когда lists выполнить, является disсобрать code объект генерируется каждым и исследуется содержимое. Вот пример разобранного объекта кода понимания списка:

#disassemble a list code object
import dis
l = "[x for x in range(10)]"
code_obj = compile(l, '<list>', 'exec')
print(code_obj)  #<code object <module> at 0x000000058DA45030, file "<list>", line 1>
dis.dis(code_obj)

 #Output:
    <code object <module> at 0x000000058D5D4C90, file "<list>", line 1>
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x000000058D5D4ED0, file "<list>", line 1>)
          2 LOAD_CONST               1 ('<listcomp>')
          4 MAKE_FUNCTION            0
          6 LOAD_NAME                0 (range)
          8 LOAD_CONST               2 (10)
         10 CALL_FUNCTION            1
         12 GET_ITER
         14 CALL_FUNCTION            1
         16 POP_TOP
         18 LOAD_CONST               3 (None)
         20 RETURN_VALUE

вот пример for объект кода цикла разобран в функции test:

#disassemble a function code object containing a `for` loop
import dis
test_list = []
def test():
    for x in range(1,10):
        test_list.append(x)


code_obj = test.__code__ #get the code object <code object test at 0x000000058DA45420, file "<ipython-input-19-55b41d63256f>", line 4>
dis.dis(code_obj)
#Output:
       0 SETUP_LOOP              28 (to 30)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_CONST               1 (1)
              6 LOAD_CONST               2 (10)
              8 CALL_FUNCTION            2
             10 GET_ITER
        >>   12 FOR_ITER                14 (to 28)
             14 STORE_FAST               0 (x)

  6          16 LOAD_GLOBAL              1 (test_list)
             18 LOAD_ATTR                2 (append)
             20 LOAD_FAST                0 (x)
             22 CALL_FUNCTION            1
             24 POP_TOP
             26 JUMP_ABSOLUTE           12
        >>   28 POP_BLOCK
        >>   30 LOAD_CONST               0 (None)
             32 RETURN_VALUE

приведенное выше сравнение показывает больше "активности", если можно, в случае for петли. Например, обратите внимание на дополнительные вызовы функции append() методfor loop функция вызова. Чтобы узнать больше о параметрах в dis выход вызова, вот официальный документация.

наконец, как предлагалось ранее, я также протестировал с file.flush() и время выполнения превышает 11 seconds. Я добавляю f.flush () перед file.write() о себе:

import os
.
.
.
for line in data_lines:
        f.flush()                #flushes internal buffer and copies data to OS buffer
        os.fsync(f.fileno())     #the os buffer refers to the file-descriptor(fd=f.fileno()) to write values to disk
        f.write(line)

более длительное время выполнения с помощью flush() можно отнести к способу обработки данных. Эта функция копирует данные из буфера программы в буфер операционной системы. Это означает, что если файл(допустим test.txt в данном случае), которая используется различными процессами и большие ломти данные добавляются в файл, вам не придется ждать, пока все данные будут записаны в файл, и информация будет доступна. Но чтобы убедиться, что данные буфера действительно записаны на диск, вам также нужно добавить:os.fsync(f.fileno()). Теперь, добавив os.fsync() увеличивает время выполнения по крайней мере 10 раз(я не просидел все это время!) поскольку он включает копирование данных из буфера в память жесткого диска. Для получения более подробной информации, go здесь.

Дальнейшая Оптимизация: есть возможность дальнейшей оптимизации процесса. Существуют библиотеки, которые поддерживают multithreading создать Process Pools и выполнить asynchronous задач . Это особенно полезно, когда функция выполняет задачу с интенсивным процессором и одновременно записывает в файл. Например, сочетание threading и list comprehensions дает быстрый возможно результат(ы):

import time
import random
import threading

start_time = time.time()

def get_seq():
    data_lines = ["%i %f\n" %(seq_id, random.random()) for seq_id in range(1,1000000)]
    with open('test.txt' ,'w') as f: 
        for line in data_lines:
            f.write(line)

set_thread = threading.Thread(target=get_seq)
set_thread.start()

print('Execution time: %s seconds' % (time.time() - start_time))

#Execution time: 0.015599966049194336 seconds

вывод: список понимания предлагают лучшую производительность по сравнению с последовательным for петли и list appends. Основная причина этого -выполнение байт-кода одной инструкции в случае понимания списка, которое быстрее, чем последовательные итеративные вызовы для добавления элементов в список как в случае for петли. Есть возможности для дальнейшая оптимизация с помощью ввода-вывода, резьбонарезной & ProcessPoolExecutor(). Вы также можете использовать комбинацию из них для достижения более быстрых результатов. Используя file.flush() зависит от вашего требования. Эту функцию можно добавить, если требуется асинхронный доступ к данным при использовании файла несколькими процессами. Хотя этот процесс может занять много времени, если вы также записываете данные из буферной памяти программы в дисковую память ОС с помощью os.fsync(f.fileno()).


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

Я думаю, что реальная проблема здесь в том, что сборщик мусора поколения работает чаще с кодом одного цикла. generational GC существует рядом с системой пересчета, чтобы периодически проверять осиротевшие объекты с ненулевыми само / циклическими ссылками.

причина, по которой это произойдет, вероятно, сложна, но мое лучшее предположение это:

  • С помощью кода с одним циклом каждая итерация неявно выделяет новую строку, а затем отправляет ее для записи в файл, после чего она отбрасывается, ее refcount идет к нулю, и, таким образом, она освобождается. Я считаю, что кумулятивный трафик alloc/dealloc является частью эвристики, которая решает, когда GC сделан, поэтому этого поведения было бы достаточно, чтобы установить этот флаг каждые столько итераций. Флаг, в свою очередь, вероятно, проверяется в любое время вашего потока быть вынужденным ждать чего-то в любом случае, потому что это отличная возможность заполнить потерянное время сбором мусора. Синхронные записи файлов-это именно такая возможность.

  • С кодом двойного цикла вы создаете строку и добавляете ее в список, снова и снова, ничего больше. Выделить, выделить, выделить. Если у вас закончится память, вы вызовете GC, но в противном случае я сомневаюсь, что вы делаете что-либо, что настроено для проверки возможности для GC. Там нет ничего, чтобы вызвать ожидание потока, переключатель контекста и т. д. Второй цикл вызывает синхронный файл ввода-вывода, где я думаю, что может произойти оппортунистический GC, но только первый вызов может вызвать его, потому что в этот момент нет дальнейшего выделения/освобождения памяти. Только после того, как весь список написан, сам список освобождается, все сразу.

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

import gc
gc.disable()

он смог уменьшить цену времени вокруг половины путем изменение следовать

for line in data_lines:
    data_file.write(line)

в:

data_file.write('\n'.join(data_lines))

вот мой диапазон тестового запуска (0, 1000000)

elapsed time 0:00:04.653065
elapsed time 0:00:02.471547

2.471547 / 4.653065 = 53 %

однако если 10 раз выше диапазона, нет большой разницы.