保存结构的最快方法 iOS / Swift

Fastest way to save structs iOS / Swift

我有这样的结构

struct RGBA: Codable {
        
   var r: UInt8
   var g: UInt8
   var b: UInt8
   var a: UInt8 
}

我想保存大量的结构 (>1_000_000)

解码

guard let history = try? JSONDecoder().decode(HistoryRGBA.self, from: data) else { return }

编码

guard let jsonData = try? encoder.encode(dataForSave) else { return false }

如何改善 encoding/decoding RAM 内存的时间和数量?

JSONEncoder/Decoder 的表现……不是很好。 ZippyJSON 是一种替代品,据说比 Foundation 的实现快 4 倍,如果您想要更好的性能和更低的内存使用量,您可能需要 Google 某种形式流媒体 JSON 解码器库。

但是,您在评论中说您不需要 JSON 格式。这很好,因为我们可以将数据更有效地存储为原始字节数组,而不是基于文本的格式,例如 JSON:

extension RGBA {
    static let size = 4 // the size of a (packed) RGBA structure
}

// encoding
var data = Data(count: history.rgba.count * RGBA.size)
for i in 0..<history.rgba.count {
    let rgba = history.rgba[i]
    data[i*RGBA.size] = rgba.r
    data[i*RGBA.size+1] = rgba.g
    data[i*RGBA.size+2] = rgba.b
    data[i*RGBA.size+3] = rgba.a
}


// decoding
guard data.count % RGBA.size == 0 else {
    // data is incomplete, handle error
    return
}
let rgbaCount = data.count / RGBA.size
var result = [RGBA]()
result.reserveCapacity(rgbaCount)
for i in 0..<rgbaCount {
    result.append(RGBA(r: data[i*RGBA.size],
                       g: data[i*RGBA.size+1],
                       b: data[i*RGBA.size+2],
                       a: data[i*RGBA.size+3]))
}

这已经比在我的机器上使用 JSON编码器快大约 50 倍(约 100 毫秒而不是约 5 秒)。

您可以通过绕过一些 Swift 的安全检查和内存管理并下降到原始指针来获得更快的速度:

// encoding
let byteCount = history.rgba.count * RGBA.size
let rawBuf = malloc(byteCount)!
let buf = rawBuf.bindMemory(to: UInt8.self, capacity: byteCount)

for i in 0..<history.rgba.count {
    let rgba = history.rgba[i]
    buf[i*RGBA.size] = rgba.r
    buf[i*RGBA.size+1] = rgba.g
    buf[i*RGBA.size+2] = rgba.b
    buf[i*RGBA.size+3] = rgba.a
}
let data = Data(bytesNoCopy: rawBuf, count: byteCount, deallocator: .free)


// decoding
guard data.count % RGBA.size == 0 else {
    // data is incomplete, handle error
    return
}
let result: [RGBA] = data.withUnsafeBytes { rawBuf in
    let buf = rawBuf.bindMemory(to: UInt8.self)
    let rgbaCount = buf.count / RGBA.size
    return [RGBA](unsafeUninitializedCapacity: rgbaCount) { resultBuf, initializedCount in
        for i in 0..<rgbaCount {
            resultBuf[i] = RGBA(r: data[i*RGBA.size],
                                g: data[i*RGBA.size+1],
                                b: data[i*RGBA.size+2],
                                a: data[i*RGBA.size+3])
        }
    }
}

我机器上的基准测试结果(我没有测试 ZippyJSON):

JSON:
Encode: 4967.0ms; 32280478 bytes
Decode: 5673.0ms

Data:
Encode: 96.0ms; 4000000 bytes
Decode: 19.0ms

Pointers:
Encode: 1.0ms; 4000000 bytes
Decode: 18.0ms

通过直接将数组从内存写入磁盘而不对其进行序列化,您可能会更快,尽管我也没有测试过。当然,当您测试性能时,请确保您在发布模式下进行测试。

考虑到您的所有属性都是 UInt8(字节),您可以使您的结构符合 ContiguousBytes 并保存其原始字节:

struct RGBA {
   let r, g, b, a: UInt8
}

extension RGBA: ContiguousBytes {
    func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
        try Swift.withUnsafeBytes(of: self) { try body([=11=]) }
    }
}

extension ContiguousBytes {
    init<T: ContiguousBytes>(_ bytes: T) {
        self = bytes.withUnsafeBytes { [=12=].load(as: Self.self) }
    }
}

extension RGBA: ExpressibleByArrayLiteral {
    typealias ArrayLiteralElement = UInt8
    init(arrayLiteral elements: UInt8...) {
        self.init(elements)
    }
}

extension Array {
    var bytes: [UInt8] { withUnsafeBytes { .init([=14=]) } }
    var data: Data { withUnsafeBytes { .init([=14=]) } }
}

extension ContiguousBytes {
    var bytes: [UInt8] { withUnsafeBytes { .init([=15=]) } }
    var data: Data { withUnsafeBytes { .init([=15=]) } }
}

extension ContiguousBytes {
    func object<T>() -> T { withUnsafeBytes { [=16=].load(as: T.self) } }
    func objects<T>() -> [T] { withUnsafeBytes { .init([=16=].bindMemory(to: T.self)) } }
}

extension ContiguousBytes {
    var rgba: RGBA { object() }
    var rgbaCollection: [RGBA] { objects() }
}

extension UIColor {
    convenience init<T: Collection>(_ bytes: T) where T.Index == Int, T.Element == UInt8 {
        self.init(red:   CGFloat(bytes[0])/255,
                  green: CGFloat(bytes[1])/255,
                  blue:  CGFloat(bytes[2])/255,
                  alpha: CGFloat(bytes[3])/255)
    }
}

extension RGBA {
    var color: UIColor { .init(bytes) }
}

let red: RGBA = [255, 0, 0, 255]
let green: RGBA = [0, 255, 0, 255]
let blue: RGBA = [0, 0, 255, 255]

let redBytes = red.bytes            // [255, 0, 0, 255]
let redData = red.data              // 4 bytes
let rgbaFromBytes = redBytes.rgba    // RGBA
let rgbaFromData = redData.rgba      // RGBA
let colorFromRGBA = red.color       // r 1.0 g 0.0 b 0.0 a 1.0
let rgba: RGBA = [255,255,0,255]    // RGBA yellow
let yellow = rgba.color             // r 1.0 g 1.0 b 0.0 a 1.0

let colors = [red, green, blue]      // [{r 255, g 0, b 0, a 255}, {r 0, g 255, b 0, a 255}, {r 0, g 0, b 255, a 255}]
let colorsData = colors.data          // 12 bytes
let colorsFromData = colorsData.rgbaCollection // [{r 255, g 0, b 0, a 255}, {r 0, g 255, b 0, a 255}, {r 0, g 0, b 255, a 255}]

edit/update:

struct LayerRGBA {
    var canvas: [[RGBA]]
}

extension LayerRGBA {
    var data: Data { canvas.data }
    init(_ data: Data) { canvas = data.objects() }
}

struct AnimationRGBA {
    var layers: [LayerRGBA]
}

extension AnimationRGBA {
    var data: Data { layers.data }
    init(_ data: Data) {
        layers = data.objects()
    }
}

struct HistoryRGBA {
    var layers: [LayerRGBA] = []
    var animations: [AnimationRGBA] = []
}

extension HistoryRGBA {
    var data: Data {
        let layersData = layers.data
        return layersData.count.data + layersData + animations.data
    }
    init(data: Data)  {
        let index = Int(Data(data.prefix(8))).advanced(by: 8)
        self.init(layers: data.subdata(in: 8..<index).objects(),
                  animations: data.subdata(in: index..<data.endIndex).objects())
    }
}

extension Numeric {
    var data: Data {
        var bytes = self
        return .init(bytes: &bytes, count: MemoryLayout<Self>.size)
    }
}

extension Numeric {
    init<D: DataProtocol>(_ data: D) {
        var value: Self = .zero
        let _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: [=28=])} )
        self = value
    }
}

游乐场测试:

let layer1: LayerRGBA = .init(canvas: [colors,[red],[green, blue]])
let layer2: LayerRGBA = .init(canvas: [[red],[green, rgba]])
let loaded: LayerRGBA = .init(layer1.data)
loaded.canvas[0]
loaded.canvas[1]
loaded.canvas[2]

let animationRGBA: AnimationRGBA = .init(layers: [layer1,layer2])
let loadedAnimation: AnimationRGBA = .init(animationRGBA.data)
loadedAnimation.layers.count // 2
loadedAnimation.layers[0].canvas[0]
loadedAnimation.layers[0].canvas[1]
loadedAnimation.layers[0].canvas[2]
loadedAnimation.layers[1].canvas[0]
loadedAnimation.layers[1].canvas[1]

let hRGBA: HistoryRGBA = .init(layers: [loaded], animations: [animationRGBA])
let loadedHistory: HistoryRGBA = .init(data: hRGBA.data)
loadedHistory.layers[0].canvas[0]
loadedHistory.layers[0].canvas[1]
loadedHistory.layers[0].canvas[2]

loadedHistory.animations[0].layers[0].canvas[0]
loadedHistory.animations[0].layers[0].canvas[1]
loadedHistory.animations[0].layers[0].canvas[2]
loadedHistory.animations[0].layers[1].canvas[0]
loadedHistory.animations[0].layers[1].canvas[1]

如果像我一样的其他人想知道使用 PropertyListEncoder/Decoder 或为 Codable 结构编写自定义 encoding/decoding 方法是否会对性能产生任何影响,那么我做了一些测试来检查它,答案是与标准 JSONEncoder/Decoder 相比,他们可以改进一点,但不会太多。我真的不能推荐这个,因为在其他答案中有更快的方法,但我认为它在某些情况下可能有用,所以我把结果放在这里。在我的测试中,将 unkeyedContainer 用于 encoding/decoding Codable 使编码速度提高了大约 2 倍,但它对解码的影响微乎其微,而使用 PropertyListEncoder/Decoder 仅产生了最小的差异,如下所示。测试代码:

struct RGBA1: Codable {
   var r: UInt8
   var g: UInt8
   var b: UInt8
   var a: UInt8
}

struct RGBA2 {
   var r: UInt8
   var g: UInt8
   var b: UInt8
   var a: UInt8
}
extension RGBA2: Codable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(r)
        try container.encode(g)
        try container.encode(b)
        try container.encode(a)
    }
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        r = try container.decode(UInt8.self)
        g = try container.decode(UInt8.self)
        b = try container.decode(UInt8.self)
        a = try container.decode(UInt8.self)
    }
}

class PerformanceTests: XCTestCase {
    var rgba1: [RGBA1] = {
        var rgba1: [RGBA1] = []
        for i in 0..<1_000_000 {
            rgba1.append(RGBA1(r: UInt8(i % 256), g: UInt8(i % 256), b: UInt8(i % 256), a: UInt8(i % 256)))
        }
        return rgba1
    }()
    var rgba2: [RGBA2] = {
        var rgba2: [RGBA2] = []
        for i in 0..<1_000_000 {
            rgba2.append(RGBA2(r: UInt8(i % 256), g: UInt8(i % 256), b: UInt8(i % 256), a: UInt8(i % 256)))
        }
        return rgba2
    }()

    func testRgba1JsonEncoding() throws {
        var result: Data?
        self.measure { result = try? JSONEncoder().encode(rgba1) }
        print("rgba1 json size: \(result?.count ?? 0)")
    }
    func testRgba1JsonDecoding() throws {
        let result = try? JSONEncoder().encode(rgba1)
        self.measure { _ = try? JSONDecoder().decode([RGBA1].self, from: result!) }
    }
    func testRgba1PlistEncoding() throws {
        var result: Data?
        self.measure { result = try? PropertyListEncoder().encode(rgba1) }
        print("rgba1 plist size: \(result?.count ?? 0)")
    }
    func testRgba1PlistDecoding() throws {
        let result = try? PropertyListEncoder().encode(rgba1)
        self.measure { _ = try? PropertyListDecoder().decode([RGBA1].self, from: result!) }
    }
    
    func testRgba2JsonEncoding() throws {
        var result: Data?
        self.measure { result = try? JSONEncoder().encode(rgba2) }
        print("rgba2 json size: \(result?.count ?? 0)")
    }
    func testRgba2JsonDecoding() throws {
        let result = try? JSONEncoder().encode(rgba2)
        self.measure { _ = try? JSONDecoder().decode([RGBA2].self, from: result!) }
    }
    func testRgba2PlistEncoding() throws {
        var result: Data?
        self.measure { result = try? PropertyListEncoder().encode(rgba2) }
        print("rgba2 plist size: \(result?.count ?? 0)")
    }
    func testRgba2PlistDecoding() throws {
        let result = try? PropertyListEncoder().encode(rgba2)
        self.measure { _ = try? PropertyListDecoder().decode([RGBA2].self, from: result!) }
    }
}

我设备上的结果:

testRgba1JsonEncoding average 5.251 sec 32281065 bytes
testRgba1JsonDecoding average 7.749 sec
testRgba1PlistEncoding average 4.811 sec 41001610 bytes
testRgba1PlistDecoding average 7.529 sec

testRgba2JsonEncoding average 2.546 sec 16281065 bytes
testRgba2JsonDecoding average 7.906 sec
testRgba2PlistEncoding average 2.710 sec 25001586 bytes
testRgba2PlistDecoding average 6.432 sec