如何压缩数据

How to compress data

我正在尝试压缩数据以提高 space 复杂性,但我不确定我是否错误地压缩了数据或错误地测量了大小。

我在 Playground 中尝试了以下方法。

import Foundation
import Compression

// Example data
struct MyData: Encodable {
    let property = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
}

// I tried using MemoryLayout to measure the size of the uncompressed data
let size = MemoryLayout<MyData>.size
print("myData type size", size) // 16

let myData = MyData()
let myDataSize = MemoryLayout.size(ofValue: myData)
print("myData instance size", myDataSize) // 16

func run() {
    // 1. This shows the size of the encoded data
    guard let encoded = try? JSONEncoder().encode(myData) else { return }
    print("myData encoded size", encoded) // 589 bytes

    /// 2. This shows the size after using a first compression method
    guard let compressed = try? (encoded as NSData).compressed(using: .lzfse) else { return }
    let firstCompression = Data(compressed)
    print("firstCompression", firstCompression) // 491 bytes

    /// 3. Second compression method (just wanted to try a different compression method)
    let secondCompression = compress(encoded)
    print("secondCompression", secondCompression) // 491 bytes
    
    /// 4. Wanted to compare the size difference between compressed and uncompressed for a bigger data so here is the array of uncompressed data.
    var myDataArray = [MyData]()
    for _ in 0 ... 100 {
        myDataArray.append(MyData())
    }
    guard let encodedArray = try? JSONEncoder().encode(myDataArray) else { return }
    print("myData encodedArray size", encodedArray) // 59591 bytes
    print("memory layout", MemoryLayout.size(ofValue: encodedArray)) // 16
    
    /// 5. Compressed array
    var compressedArray = [Data]()
    for _ in 0 ... 100 {
        guard let compressed = try? (encoded as NSData).compressed(using: .lzfse) else { return }
        let data = Data(compressed)
        compressedArray.append(data)
    }
    guard let encodedCompressedArray = try? JSONEncoder().encode(compressedArray) else { return }
    print("myData compressed array size", encodedCompressedArray) // 66661 bytes
    print("memory layout", MemoryLayout.size(ofValue: encodedCompressedArray)) // 16

    /// 6. Compression using lzma
    var differentCompressionArray = [Data]()
    for _ in 0 ... 100 {
        guard let compressed = try? (encoded as NSData).compressed(using: .lzma) else { return }
        let data = Data(compressed)
        differentCompressionArray.append(data)
    }
    guard let encodedCompressedArray2 = try? JSONEncoder().encode(differentCompressionArray) else { return }
    print("myData compressed array size", encodedCompressedArray2) // 60702 bytes
    print("memory layout", MemoryLayout.size(ofValue: encodedCompressedArray2)) // 16
}

run()

// The implementation for the second compression method
func compress(_ sourceData: Data) -> Data {
    let pageSize = 128
    var compressedData = Data()
    
    do {
        let outputFilter = try OutputFilter(.compress, using: .lzfse) { (data: Data?) -> Void in
            if let data = data {
                compressedData.append(data)
            }
        }
        
        var index = 0
        let bufferSize = sourceData.count
        
        while true {
            let rangeLength = min(pageSize, bufferSize - index)
            
            let subdata = sourceData.subdata(in: index ..< index + rangeLength)
            index += rangeLength
            
            try outputFilter.write(subdata)
            
            if (rangeLength == 0) {
                break
            }
        }
    }catch {
        fatalError("Error occurred during encoding: \(error.localizedDescription).")
    }
    
    return compressedData
}

MemoryLayout 对象似乎对测量编码数组的大小没有帮助,无论它们是否被压缩。我不确定如何在不使用已经压缩数据的 JSONEncoder 对它们进行编码的情况下测量 struts 的结构或数组。

MyData(#1、#2 和#3)的单个实例的 before/after 压缩似乎表明数据已从 589 字节正确压缩到 491 字节.但是,未压缩数据数组和压缩数据数组(#4、#5)之间的比较似乎表明,压缩后的大小从 59591 增加到 66661。

最后,我尝试使用不同的压缩算法 lzma (#6)。它将大小减小到 60702,低于之前的压缩,但它仍然不小于未压缩的数据。

首先要消除一些混淆:MemoryLayout 为您提供有关编译时类型布局的大小和结构的信息,但不能用于确定数量Array 值在运行时需要的存储空间,因为 Array 结构本身的大小 不取决于它包含多少数据。

高度简化,Array 值的布局如下所示:

┌─────────────────────┐                        
│        Array        │                        
├──────────┬──────────┤    ┌──────────────────┐
│  length  │  buffer ─┼───▶│     storage      │
└──────────┴──────────┘    └──────────────────┘
  1 word /   1 word /                          
  8 bytes    8 bytes                           
 └─────────┬─────────┘
           └─▶ MemoryLayout<Array<UInt8>>.size

Array 值存储它的长度,或 count(与一些标志混合,但我们不需要担心)和指向实际 space 它包含的项目的存储位置。这些项目不存储为 Array 本身 的一部分,而是单独存储在 Array 分配的内存中] 到。无论 Array“包含”10 个值还是 100000 个值,Array 结构的大小都保持不变:长度为 1 个字(或 64 位系统上为 8 个字节),以及 1 个字指向实际底层存储的指针。 (但是,存储缓冲区的大小 正好 由它在运行时能够包含的元素数量决定。)

实际上,由于桥接和其他原因,Array比这复杂得多,但这是基本要点;这就是为什么您每次只能看到 MemoryLayout.size(ofValue:) return 相同的数字。 [顺便说一下,出于类似原因,String 的大小与 Array 相同,这就是为什么 MemoryLayout<MyData>.size 也报告了 16。]

为了知道 ArrayData 有效占用了多少字节,询问他们的 .count 就足够了:Array<UInt8>Data 都是 UInt8 值(字节)的集合,它们的 .count 将反映有效存储在其底层存储中的数据量。


至于步骤(4)和(5)之间的大小增加,请注意

  • 第 4 步取 100 个 MyData 的副本并将它们连接在一起,然后将它们转换为 JSON,而
  • 第 5 步获取 100 个 单独压缩的 MyData 实例,将 那些 连接在一起,然后 re-coverts 他们 JSON

与第 4 步相比,第 5 步有一些问题:

  1. 压缩极大地受益于数据的重复:压缩并重复 100 次的数据位不会像重复 100 次然后压缩的数据位一样紧凑,因为每一轮压缩都无法从知道在它之前有另一个数据副本中获益。举个简单的例子:
    • 假设我们想使用 run-length encoding 的形式来压缩字符串 Hello:我们无能为力,除了可能将其转换为 Hel{2}o(其中 {2} 表示最后一个字符重复 2 次)
    • 如果我们压缩 Hello 并加入它 3 次,我们可能会得到 Hel{2}oHel{2}oHel{2}o,
    • 但是如果我们先加入Hello3次再压缩,就可以得到{Hel{2}o}{3},这样就紧凑多了
  2. 压缩通常还需要插入一些有关数据压缩方式的信息,以便以后能够识别和解压缩数据。通过压缩 MyData 100 次并加入所有这些实例,您将重复该元数据 100 次
  3. 即使在压缩您的 MyData 个实例之后,re-representing 它们仍然会 JSON 减少 它们的压缩程度,因为它不能代表二进制数据。相反,它必须将每个 Data blob 转换为 Base64-encoded string,这会导致它再次 增长

在这些问题之间,您的数据不断增长并不奇怪。您真正想要的是对步骤 4 的修改,即压缩 joined 数据:

guard let encodedArray = try? JSONEncoder().encode(myDataArray) else { fatalError() }
guard let compressedEncodedArray = try? (encodedArray as NSData).compressed(using: .lzma) else { fatalError() }
print(compressedEncodedArray.count) // => 520

显着优于

guard let encodedCompressedArray = try? JSONEncoder().encode(compressedArray) else { fatalError() }
print(encodedCompressedArray.count) // => 66661

顺便说一句:您似乎不太可能在实践中实际使用 JSONEncoder 以这种方式 join 数据,这只是为了测量 —但如果你真的是,请考虑其他机制来做到这一点。以这种方式将二进制数据转换为 JSON 是非常低效的 storage-wise,并且通过更多关于您在实践中可能实际需要的信息,我们可以推荐一种更有效的方法来执行此操作。

如果你在实践中实际做的是编码一个 Encodable 对象树,然后压缩 一次,那完全没问题。