在Swift中实现快速JSON解析的道路

在过去的几年中,我已经编写了很多库。 但是没有一个图书馆比BSON更具教育性和趣味性。 BSON是MongoDB制作的一种格式,类似于JSON,但为二进制。 它可以非常有效地进行读写。

我们的BSON实现采用了我们收到的原始缓冲区,并且仅使用原始缓冲区就可以高性能获取任何您想要的东西。 因此,如果经常从套接字接收此数据,则无需复制缓冲区。 当我尝试使用JSON时,结果截然不同。

BSON是一种聪明的格式

Document的格式大致与此类似,但采用二进制形式: [(BSONType, KeyString, BSONValue)] 。 任何类型为空的值都为空(否)。 任何具有恒定长度的类型(Int,布尔值)都可以进行内联解析和变异,而不会遇到任何麻烦。 如果您有一个长度可变的类型,例如数组,(sub)Document,String或Binary Blob,则情况会稍有变化。

这些值在数据前都有一个32位整数,因此,如果您确定不需要此键,则可以跳过该键而不必读取实际值。 这样,如果您有一个具有5个键的用户,而只需要键4,则在4之前,几乎不会花任何力气在键5上花费任何精力。

在我们的库中,我们保留了所有类型的少量缓存以及在它们处找到的偏移量。 因此,如果我们决定需要键3,则甚至无需在此之前搜索键。 该缓存是一个很小的优化,但实际上可以在更大的数据集中获得回报。

JSON是慢速格式

虽然对人类来说解析起来非常容易,但对于计算机而言则相反。 如果您收到带有嵌套对象的对象,则计算机需要先解析所有内容,然后才能知道当前对象的结束位置。 仅以原始格式将JSON存储在内存中是提高性能的好主意。

因此,我开始考虑BSON,得出的结论是,在这里,我们也可以选择不复制整个String,而是将缓存保留到原始缓冲区。 这样,如果您不需要JSON中的所有键和值,则几乎不需要花费任何精力来读取那些未使用的值。 因此,我开始考虑构建缓存的想法,该缓存几乎与BSON相同,但有一些小的更改。

  • 布尔值存储为单独的类型(boolTrue和boolFalse)。 如果布尔值的文本已经在索引中被扫描,则无需解析。
  • 字符串不能包含转义字符。 如果未对String进行转义,则不必执行这些转义的检查/转换。 我们需要检查它,以查看转义字符是否始终以转义符结尾。
  • 数字根据文字中的字符将其类型存储为整数或浮点类型。 这可以减轻解析。
  • 字符串和数字可以延迟解析,我们不需要复制它们。 这主要节省了内存使用。

因此,在实现此功能之后,我很高兴看到缓存在相同的JSON数据方面优于Foundation。 但是我渴望更多。

比BSON更好的缓存

当我查看事件探查器时,我注意到几乎所有性能都是要读取和写入缓存。 我认为这很有意义,但是很多内存操作不需要这么重。 那时我想起了缓存的结构。

缓存将为数组和对象创建键和值的单独列表。 这意味着将为每个对象和数组进行新的内存分配。 每次在Codable中遍历该结构时,都需要将其加载到您的CPU中。 因此解决方案是减少内存使用量,但是如何呢?

我想到的是缓存的单个指针。 如果我将所有数据存储在单个缓冲区中(例如BSON格式本身),则可以轻松遍历它们。 未使用的数据将几乎无害。

经过3点的工作,缓存已完成。 结果是惊人的。 测试使用初始缓存在构建中花费了25ms,而使用新缓存仅花费了17ms。

缺点

到目前为止,只有一个缺点。 缓存本身仅通过存储偏移量和长度来使用原始JSON数据使用的内存的70%。 在下一次更新中,我将研究使偏移量和长度更紧凑,尽管这会减少可解析的JSON数据的大小。