簡體   English   中英

在Python中生成非常大的文本文件的時間性能

[英]Time performance in Generating very large text file in Python

我需要生成一個非常大的文本文件。 每一行都有一個簡單的格式:

Seq_num<SPACE>num_val
12343234 759

我們假設我將生成一個包含1億行的文件。 我嘗試了兩種方法,令人驚訝的是它們提供了非常不同的時間性能。

  1. 對於超過100米的循環。 在每個循環中,我創建短字符串seq_num<SPACE>num_val ,然后我將其寫入文件。 這種方法需要花費很多時間。

     ## APPROACH 1 for seq_id in seq_ids: num_val=rand() line=seq_id+' '+num_val data_file.write(line) 
  2. 對於超過100米的循環。 在每個循環中,我創建短字符串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必須花費更少的時間。 什么提示我缺少什么?

技術上非常模糊的術語很多 很少 :)基本上如果你無法測量它,你就無法改進它。

為簡單起見,我們有一個簡單的基准測試, 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 with 2 for循環:

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如何實現寫入文件 對於文本文件的open()函數使用BufferedWriter .The open函數接受第三個參數是緩沖區的大小。 這是有趣的部分:

傳遞0以切換緩沖關閉(僅允許在二進制模式下),1選擇行緩沖(僅在文本模式下可用),並且整數> 1以指示固定大小的塊緩沖區的大小(以字節為單位)。 如果沒有給出緩沖參數,則默認緩沖策略的工作方式如下:

二進制文件以固定大小的塊緩沖; 使用啟發式方法選擇緩沖區的大小,嘗試確定底層設備的“塊大小”並回退到io.DEFAULT_BUFFER_SIZE。 在許多系統上,緩沖區通常為4096或8192字節長。

所以,我們可以修改loop1.py並使用行緩沖:

data_file = open('file.txt', 'w', 1)

事實證明這很慢:

$ python3 loop3.py 
elapsed time 0:00:02.470757

為了優化寫入時間,我們可以根據需要調整緩沖區大小。 首先我們檢查行大小,以字節為單位: len(line.encode('utf-8')) ,它給出了11個字節。

將緩沖區大小更新為我們預期的行大小(以字節為單位):

data_file = open('file.txt', 'w', 11)

我寫得很快:

elapsed time 0:00:00.669622

根據您提供的詳細信息,很難估計發生了什么。 也許用於估計塊大小的啟發式方法在您的計算機上運行不正常。 無論如何,如果您正在編寫固定行長度,則可以輕松優化緩沖區大小。 您可以通過利用flush()進一步優化對文件的寫入。

結論 :通常,為了更快地寫入文件,您應該嘗試編寫與文件系統上的塊大小相對應的大量數據 - 這正是Python方法open('file.txt', 'w')是試圖做。 在大多數情況下,您使用默認值是安全的,微基准測試的差異是微不足道的。

您正在分配大量的字符串對象,這些對象需要由GC收集。 正如@ kevmo314所建議的那樣,為了執行公平比較,您應該為loop1.py禁用GC:

gc.disable()

由於GC可能會嘗試在迭代循環時刪除字符串對象(您沒有保留任何引用)。 雖然秒方法保持對所有字符串對象的引用,GC最后收集它們。

以下是@Tombart對優雅答案的擴展以及一些進一步的觀察。

有一個目標:優化從循環中讀取數據的過程,然后將其寫入文件,讓我們開始:

在所有情況下,我將使用with語句打開/關閉文件test.txt 當執行其中的代碼塊時,此語句自動關閉該文件。

另一個需要考慮的重點是Python基於操作系統處理文本文件的方式。 來自文檔

注意 :Python不依賴於底層操作系統的文本文件概念; 所有處理都由Python本身完成,因此與平台無關。

這意味着在Linux / Mac或Windows操作系統上執行時,這些結果可能只會略有不同。 稍微變化可能是由於在腳本執行期間同時使用相同文件的其他進程或文件上發生的多個IO進程,一般CPU處理速度等。

我提出了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() 原因是: []list()快3倍。 以下是對此行為的解釋: 為什么[]比list()更快? 討論的主要內容是:雖然[]創建為字節碼對象並且是單個指令 ,但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循環執行和list執行時發生的事情的一種有趣的方法是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

這是在函數test反匯編的for循環代碼對象的示例:

#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循環的情況下。 例如,注意在for循環函數調用中對append()方法的append()函數調用。 要了解有關dis調用輸出中的參數的更多信息,請參閱官方文檔

最后,如前所述,我還使用file.flush()進行測試,執行時間超過11 seconds 我在file.write()語句之前添加了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次 (我沒有經歷過整個時間!),因為它涉及將數據從緩沖區復制到硬盤內存。 有關詳細信息,請轉到此處

進一步優化 :可以進一步優化流程。 有些庫可用於支持multithreading ,創建Process Pools和執行asynchronous任務。 當函數執行CPU密集型任務並同時寫入文件時,這尤其有用。 例如, threadinglist comprehensions threading的組合可以提供最快的結果:

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 append s相比,List comprehensions提供了更好的性能。 這背后的主要原因是在列表推導的情況下執行單指令字節碼 ,這比在for循環的情況下將項追加到列表順序迭代調用更快。 使用asynciothreadingProcessPoolExecutor()可以進一步優化。 您還可以使用這些組合來實現更快的結果。 使用file.flush()取決於您的要求。 當您在多個進程使用文件時需要異步訪問數據時,可以添加此函數。 雖然,如果您還使用os.fsync(f.fileno())將數據從程序的緩沖存儲器寫入OS的磁盤存儲器,則此過程可能需要很長時間。

考慮方法2,我想我可以假設你有所有行的數據(或者至少在大塊) 之前,你需要將它寫入文件。

其他答案很棒,閱讀它們確實非常具有形式,但兩者都專注於優化文件編寫或避免第一個for循環替換列表理解(已知更快)。

他們錯過了你在for循環中迭代來編寫文件的事實,這不是必需的。

而不是這樣做,通過增加內存的使用(在這種情況下是可承受的,因為1億行文件大約600 MB),你可以通過使用格式化或連接功能,以更有效的方式創建一個字符串。 python str,然后將大字符串寫入文件。 還依賴列表理解來獲取要格式化的數據。

使用@Tombart答案的loop1和loop2,我得到的elapsed time 0:00:01.028567elapsed 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的過濾列表見這里 )。
  • 如果可能,通過內存或實現約束,嘗試使用formatjoin函數一次創建和編碼字符串內容。
  • 如果可能的話,代碼保持可讀,使用內置的功能,避免for循環。 例如,使用列表的extend函數而不是迭代和使用append 事實上,以前的兩點都可以看作是這句話的例子。

備注。 雖然這個答案本身可以被認為是有用的,但它並沒有完全解決這個問題,這就是為什么問題中的兩個循環選項似乎在某些環境中運行得更快的原因。 為此,也許下面的@Aiken Drum的回答可以為這件事帶來一些啟示。

這里的其他答案提供了很好的建議,但我認為實際問題可能有所不同:

我認為這里真正的問題是分代垃圾收集器使用單循環代碼運行得更頻繁。 生成GC與引用計數系統一起存在,以定期檢查具有非零自/循環引用的孤立對象。

這種情況發生的原因可能很復雜,但我最好的猜測是:

  • 使用單循環代碼,每次迭代都隱式地分配一個新字符串,然后將其發送以寫入文件,之后它被放棄,其refcount變為零,因此它被解除分配。 我相信累積的alloc / dealloc流量是啟發式的一部分,它決定GC何時完成,所以這種行為足以在每那么多次迭代中設置該標志。 反過來,當你的線程被強制等待某個東西時,可能會檢查該標志,因為這是一個用垃圾收集來填補浪費時間的絕佳機會。 同步文件寫入正是這種機會。

  • 使用雙循環代碼,您將創建一個字符串並將其一遍又一遍地添加到列表中。 分配,分配,分配。 如果你的內存不足,你就會觸發一個GC,但是我懷疑你是在做什么設置來檢查GC的機會。 沒有什么可以導致線程等待,上下文切換等。第二個循環調用同步文件I / O,我認為機會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倍,則沒有太大區別。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM