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

Reading Binary File Piecemeal and Converting to Integers With Memory Efficiency

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

在此处插入文件 IO Rant。

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

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 { [=10=].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,我也只能读取一个 8GB 的​​文件,因为它必须双倍消耗内存。

如您所见,我的代码没有使用此代码。对于读取,我只是将文件读入 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
            }
        }

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

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

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

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

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

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

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

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

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

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

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