读取二进制文件碎片并转换为具有内存效率的整数

Posted

技术标签:

【中文标题】读取二进制文件碎片并转换为具有内存效率的整数【英文标题】:Reading Binary File Piecemeal and Converting to Integers With Memory Efficiency 【发布时间】:2022-01-16 11:45:36 【问题描述】:

使用 Swift,我需要从二进制文件中读取整数,但由于它们的大小,无法将整个文件读入内存。我有 61G 字节(77 亿整数)的数据写入十几个不同大小的文件中。最大的是 18G 字节(22 亿整数)。有些文件可能会完全读入内存,但最大的文件大于可用 RAM。

在此处插入文件 IO Rant。

我已经编写了一次将文件写入 1000 万字节的代码,并且运行良好。我把它写成一个类,但其余代码都不是面向对象的。这不是一个应用程序,因此没有空闲时间来进行内存清理。代码如下:

class BufferedBinaryIO 
    var data = Data(capacity: 10000000)
    var data1:Data?
    let fileName:String!
    let fileurl:URL!
    var fileHandle:FileHandle? = nil
    var (forWriting,forReading) = (false,false)
    var tPointer:UnsafeMutablePointer<UInt8>?
    var pointer = 0

    init?(forWriting name:String) 
        forWriting = true
        fileName = name
        fileurl =  URL(fileURLWithPath:fileName)
        if FileManager.default.fileExists(atPath: fileurl.path) 
            try! fileHandle = FileHandle(forWritingTo: fileurl)
            if fileHandle == nil 
                print("Can't open file to write.")
                return nil
            
        
        else 
            // if file does not exist write data for the first time
            do
                try data.write(to: fileurl, options: .atomic)
                try fileHandle = FileHandle(forWritingTo: fileurl)
             catch 
                print("Unable to write in new file.")
                return nil
            
        
    
    
    init?(forReading name:String) 
        forReading = true
        fileName = name
        fileurl =  URL(fileURLWithPath:fileName)
        if FileManager.default.fileExists(atPath: fileurl.path) 
            try! fileHandle = FileHandle(forReadingFrom: fileurl)
            if fileHandle == nil 
                print("Can't open file to write.")
                return nil
            
        
        else 
            // if file does not exist write data for the first time
            do
                try fileHandle = FileHandle(forWritingTo: fileurl)
             catch 
                print("Unable to write in new file.")
                return nil
            
        
    
    
    deinit 
        if forWriting 
            fileHandle?.seekToEndOfFile()
            fileHandle?.write(data)
        
        try? fileHandle?.close()
            
    
    
    func write(_ datum: Data) 
        guard forWriting else  return 
        self.data.append(datum)
        if data.count == 10000000 
            fileHandle?.write(data)
            data.removeAll()
        
    
    
    func readInt() -> Int? 
        if data1 == nil || pointer == data1!.count 
            if #available(macOS 10.15.4, *) 
                //data1?.removeAll()
                //data1 = nil
                data1 = try! fileHandle?.read(upToCount: 10000000)
                pointer = 0
             else 
                // Fallback on earlier versions
            
        
        if data1 != nil && pointer+8 <= data1!.count 
            let retValue = data1!.withUnsafeBytes  $0.load(fromByteOffset: pointer,as: Int.self) 
            pointer += 8
           // data.removeFirst(8)
            return retValue
         else 
            print("here")
        

        return nil
    

正如我所说,写入文件工作正常,我可以从文件中读取,但我遇到了问题。

一些读取二进制文件并将其转换为各种类型的解决方案使用如下代码:

let rData = try! Data(contentsOf: url)
let tPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: rData.count)
rData.copyBytes(to: tPointer, count: rData.count)

第一行读取整个文件,消耗相同数量的内存,接下来的两行使内存消耗翻倍。因此,即使我有 16G 字节的 Ram,我也只能读取 8G 字节的文件,因为它必须双倍消耗内存。

如您所见,我的代码没有使用此代码。对于读取,我只是将文件读入 data1,一次 1000 万字节,然后像使用常规数据类型一样使用 data1 并访问它,可以很好地读取数据,而不会增加一倍的内存使用量。

使用此代码的程序主体中的代码如下所示:

        file loop .... 

            let string = String(format:"~path/filename.data")
            let dataPath = String(NSString(string: string).expandingTildeInPath)
            let fileBuffer = BufferedBinaryIO(forReading: dataPath)
            
            while let value = fileBuffer!.readInt() 
                loop code
            
        

这是我的问题:此代码用于将文件读入 Ints,但在 readInt 中,当执行下一个 fileHandle?.read 时,该代码不会从前一个 fileHandle?.read 释放内存。因此,当我浏览文件时,每次填充缓冲区时内存消耗都会增加 1000 万,直到程序崩溃。

请原谅我的代码,因为它正在进行中。我不断更改它以尝试不同的方法来解决此问题。我使用 data1 作为代码读取部分的可选变量,认为将其设置为 nil 会释放内存。当我只是重写它时,它会做同样的事情。

话虽如此,如果可行的话,这将是一种很好的编码方式。

所以问题是我是否有内存保留周期,或者是否有我需要在 data1 上使用的魔法 bean 让它停止这样做?

提前感谢您考虑这个问题。

【问题讨论】:

对于任何想要复制上述代码的人,您需要进行 Duncan C 建议的更改。此外,此代码在开始写入之前不会清空/删除文件。我一直在手动进行。我需要完善代码以供一般使用。 【参考方案1】:

您没有显示实际从文件中读取的代码,因此很难确定发生了什么。

从您展示的代码中,我们可以看出您正在使用 FileHandle,它允许随机访问文件并读取任意大小的数据块。

假设您做对了这部分并且一次读取 1000 万字节,您的问题可能是 ios 和 Mac OS 处理内存的方式。对于某些事情,操作系统将不再使用的内存块放入“自动释放池”中,当您的代码返回并且事件循环得到服务时,该池将被释放。如果您正在同步处理数 GB 的文件数据,它可能没有机会在下一次传递之前释放内存。

(详细解释 Mac OS/iOS 内存管理以涵盖自动释放将非常复杂。如果您有兴趣,我建议您查看 Apple 手动引用计数和自动引用计数,即 ARC,并查找结果解释“幕后”发生的事情。)

尝试将读取 1000 万字节数据的代码放入 autoreleasePool() 语句的闭包中。这将导致任何自动释放的内存实际被释放。类似于下面的伪代码:

while (more data) 
    autoreleasepool 
        // read a 10 million byte block of data
        // process that block
    

【讨论】:

Duncan C,感谢这解决了问题。当我不寻找与文件 IO 相关的任何内容时,我曾在网上多次看到此声明。它不在 Swift 编程语言手册,Swift 5.3 的补充中,他们在其中讨论了强引用循环。所以我很可能不会很快自己找到它。

以上是关于读取二进制文件碎片并转换为具有内存效率的整数的主要内容,如果未能解决你的问题,请参考以下文章

IO-字符输入流

PHP如何将从二进制文件中读取的字节转换为数字

有没有更快的方法将大文件从十六进制转换为二进制,二进制转换为int?

如何在Javascript中将整数转换为具有固定长度的十六进制?

C ++读取二进制文件并转换为十六进制

从二进制文件中读取并转换为双精度?