我正在撰寫一個相當大的文本檔案(它實際上更像是 ascii 編碼的資料),而且它......非常慢。并且占用大量記憶體。
這是我用來測驗如何更快地寫入檔案的代碼的簡約版本。writeFileIncrementally
在 for 回圈中一次寫入一行,同時writeFileFromBigData
創建一個大字串,然后將其轉儲到磁盤。我完全期待writeFileFromBigData
更快,但它快了20 倍!這比我預期的要多一點。對于size=10_000_000
,增量寫入需要 20-25 秒,一次性寫入需要 1-1.5 秒。另外,增量版本實際上分配了越來越多的記憶體。到最后,它已進入 GiB 范圍。我不明白這里發生了什么。
func writeFileIncrementally(toUrl url: URL, size: Int) {
// ensure file exists and is empty
try? "".write(to: url, atomically: true, encoding: .ascii)
guard let handle = try? FileHandle(forWritingTo: url) else {return}
defer {
handle.closeFile()
}
for i in 0..<size {
let s = "\(i)\n"
handle.write(s.data(using: .ascii)!)
}
}
func writeFileFromBigData(toUrl url: URL, size: Int) {
let s = (0..<size).map{String($0)}.joined(separator: "\n")
try? s.write(to: url, atomically: true, encoding: .ascii)
}
將其與 Python 中的相同內容進行比較。在 Python 中,create-string-then-write-it 也更快。這是合理的,但 Python 的不同之處在于增量撰寫它需要大約 2.7 秒(大約 98% 的用戶時間),而一次性撰寫它大約需要 1 秒(包括創建字串)。此外,增量版本具有恒定的記憶體使用量。在寫入檔案時它不會上升。
def writeFileIncrementally(path, size):
with open(path, "w ") as f:
for i in range(size):
f.write(f"{i}\n")
def writeFileFromBigData(path, size):
with open(path, "w ") as f:
f.write("\n".join(str(i) for i in range(size)))
所以我的問題是雙重的:
- 為什么我的
writeFileIncrementally
函式這么慢,為什么它使用這么多記憶體?我希望能夠增量寫入以減少記憶體使用。 - 是否有更好的方法可以在 Swift 中增量撰寫大型文本檔案?
uj5u.com熱心網友回復:
有關記憶,請參閱 Duncan C 的答案。你需要一個自動釋放池。但是為了速度,你有一個小問題和一個大問題。
小問題是這一行:
handle.write(s.data(using: .ascii)!)
重寫將節省大約 40% 的時間(在我的測驗中從 27 秒到 17 秒):
handle.write(Data(s.utf8))
字串通常在內部以 UTF8 存盤。雖然 ASCII 是其中的一個完美子集,但您的代碼需要檢查任何不是 ASCII 的內容。使用.utf8
通常可以直接抓取內部緩沖區。它還避免了創建和解包 Optional。
但是 17s 仍然比 1-2s 多很多。那是因為你的大問題。
每次呼叫write
都必須將資料一直獲取到作業系統的檔案緩沖區。不是一直到磁盤,但它仍然是一項昂貴的操作。除非資料很珍貴,否則您通常希望將其分塊成更大的塊(4k 非常常見)。如果你這樣做,寫入時間會下降到 1.5 秒:
let bufferSize = 4*1024
var buffer = Data(capacity: bufferSize)
for i in 0..<size {
autoreleasepool {
let s = "\(i)\n"
buffer.append(contentsOf: s.utf8)
if buffer.count >= bufferSize {
handle.write(buffer)
buffer.removeAll(keepingCapacity: true)
}
}
}
// Write the final buffer
handle.write(buffer)
這與我系統上的“大資料”功能的 1.1 秒“非常接近”。仍然有很多記憶體分配和清理正在進行。根據我的經驗,至少最近,[UInt8]
它比 Data 快得多。我不確定這是否總是正確的,但我最近在 Mac 上的所有測驗都是這樣。因此,使用較新的write(contentsOf:)
界面撰寫是:
let bufferSize = 4*1024
var buffer: [UInt8] = []
buffer.reserveCapacity(bufferSize)
for i in 0..<size {
autoreleasepool {
let s = "\(i)\n"
buffer.append(contentsOf: s.utf8)
if buffer.count >= bufferSize {
try? handle.write(contentsOf: buffer)
buffer.removeAll(keepingCapacity: true)
}
}
}
// Write the final buffer
try? handle.write(contentsOf: buffer)
這比大資料功能要快,因為它不需要生成資料。(在我的機器上為 830 毫秒)
但是等等,它會變得更好。此代碼不需要自動釋放池,如果您洗掉它,我可以在 730 毫秒內撰寫此檔案。
let bufferSize = 4*1024
var buffer: [UInt8] = []
buffer.reserveCapacity(bufferSize)
for i in 0..<size {
let s = "\(i)\n"
buffer.append(contentsOf: s.utf8)
if buffer.count >= bufferSize {
try? handle.write(contentsOf: buffer)
buffer.removeAll(keepingCapacity: true)
}
}
// Write the final buffer
try? handle.write(contentsOf: buffer)
但是 Python 呢?為什么它不需要緩沖區來快速?因為它默認為您提供緩沖區。您的open
呼叫回傳一個帶有 8k 緩沖區的 BufferedWriter,其作業方式或多或少類似于上述代碼。您需要以二進制模式寫入并通過buffering=0
將其關閉。有關詳細資訊,請參閱檔案open
。
uj5u.com熱心網友回復:
我不確定為什么增量寫作版本這么慢。
但是,如果您擔心記憶體使用,您可以通過呼叫以下方法來包裝您的內部回圈,從而使您的記憶體占用更小autoreleasepool()
:
for i in 0..<size {
autoreleasepool {
let s = "\(i)\n"
handle.write(s.data(using: .ascii)!)
if i.isMultiple(of: 100000) {
print(i)
}
}
}
(在內部,Swift 的 ARC 記憶體管理有時會將堆上的臨時存盤分配為“自動釋放”,這意味著它會一直保留在記憶體中,直到當前呼叫鏈回傳并且應用程式重新訪問事件回圈。如果您有一個分配整個的處理回圈一堆區域變數,它們可以在堆上累積,直到你完成并回傳。然而,如果你推高設備的記憶體限制,這真的只是一個問題。)
編輯:
我認為這可能是過早優化的情況。在我看來,10,000,000 個專案的“一次全部寫入”的最大記憶體消耗約為 150 mb,這對于能夠運行當前 iOS 版本的設備來說不是問題。只需使用“一次全部撰寫”版本即可完成。如果您需要一次撰寫數十億行,則撰寫混合代碼,一次將其分解為 1000 萬行,并將每個塊附加到檔案中。autoreleasepool()
(如上所示,內部回圈包含在對 的呼叫中。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/528933.html
標籤:迅速文件-io