3 个版本

0.1.3 2021 年 9 月 3 日
0.1.2 2021 年 8 月 24 日
0.1.1 2021 年 4 月 20 日
0.1.0 2020 年 11 月 21 日
0.0.1 2020 年 11 月 19 日

#516编码

Download history 1410/week @ 2024-03-13 1598/week @ 2024-03-20 1270/week @ 2024-03-27 1181/week @ 2024-04-03 1147/week @ 2024-04-10 1227/week @ 2024-04-17 856/week @ 2024-04-24 855/week @ 2024-05-01 957/week @ 2024-05-08 1208/week @ 2024-05-15 1731/week @ 2024-05-22 1378/week @ 2024-05-29 1082/week @ 2024-06-05 1078/week @ 2024-06-12 1441/week @ 2024-06-19 784/week @ 2024-06-26

4,726 每月下载量
用于 9 个 crate(6 个直接使用)

MIT/Apache

550KB
4.5K SLoC

包含 (Zip 文件,59KB) comparison.xlsx

ijson

CI Status Documentation crates.io codecov

该 crate 提供了 serde-jsonValue 类型的替代品,在内存效率上显著更高。

大致而言,在反序列化值时,它通常使用的内存是 serde-json 的一半,并且克隆值的内存占用比 serde-json 小 7 倍以上。

内存节省

以下图表显示了内存节省作为 JSON 大小(以字节为单位)的函数。该 JSON 使用位于 test_data 文件夹中的模板,并通过 JavaScript 工具 dummyjson 随机生成。

Peak memory usage when deserializing Total allocations when deserializing Memory overhead of cloning Total allocations when cloning

您可以通过从 NPM 安装 dummy-json 并在根目录中运行以下命令自行重现它们

cargo run --example generate --release
cargo run --example comparison --release > comparison.csv

comparison.xlsx Excel 文件使用此 CSV 作为数据源来生成图表。

使用方法

IValue

此 crate 提供的主要类型是 IValue 类型。它保证是指针大小的,并且有一个利基(因此 Option<IValue> 也保证是指针大小的)。

serde_json::Value 相比,此类型是结构体而不是枚举,这是实现重要大小缩减所必需的。这意味着您不能直接在 IValue 上进行 match 以确定其类型。

相反,一个 IValue 提供了多种获取内部类型的方法

  • 使用 IValue::destructure[{_ref,_mut}]() 进行解构

    这些方法返回包装枚举,您可以直接匹配,因此这些方法是匹配 Value 的最直接替代品。

  • 使用 IValue::as_{array,object,string,number}[_mut]() 进行借用

    这些方法返回对应引用的 Option,如果类型匹配预期的类型。这些方法存在于不是 Copy 的变体中。

  • 使用 IValue::into_{array,object,string,number}() 进行转换

    这些方法返回对应类型的 Result(如果类型不是预期的,则返回原始的 IValue)。这些方法也存在于不是 Copy 的变体中。

  • 使用 IValue::to_{bool,{i,u,f}{32,64}}[_lossy]}() 获取信息

    这些方法返回对应类型的 Option。这些方法存在于返回值将是 Copy 的类型中。

您还可以使用这些方法之一在不具体访问它的情况下检查内部值的类型

  • 使用 IValue::is_{null,bool,number,string,array,object,true,false}() 进行检查

    这些方法适用于所有类型。

  • 使用 IValue::type_() 获取类型

    此方法返回 ValueType 枚举,该枚举具有六个 JSON 类型的每个类型的变体。

INumber

INumber 类型表示 JSON 数字。它与任何特定的表示解耦,并在内部使用多种表示。无法确定内部表示:相反,调用者应使用其中一个可错误的 to_xxx 函数转换数字,并处理数字无法转换为所需类型的情况。

特殊浮点值(例如 NaN、Infinity 等)不能存储在 INumber 中。

尽管 INumber 认为以下两个数字(即 2.0 和 2)相同(即它们会相等),但它允许您使用方法 INumber::has_decimal_point() 来区分它们。也就是说,在 2.0 上调用 to_i32 将成功并返回值 2

目前,INumber 可以存储任何可以用 f64i64u64 表示的数字。预计在未来,它将进一步扩展以存储任意精度的整数和可能的十进制数,但目前并非如此。

任何可以用 i8u8 表示的数字都可以存储在 INumber 中,而不需要进行堆分配(因此 JSON 字节数组相对高效)。24 位及以下的整数可以使用 4 字节堆分配来存储。

IString

IString 类型是一个内部字符串,是不可变的,这也是该crate名称的由来。

克隆 IString 成本较低,并且可以轻松地从 &strString 类型转换而来。IString 之间的比较是一个简单的指针比较。

IString 的内存是引用计数的,因此与许多字符串内部库不同,内存不会在新的字符串被内部化时泄漏。内部化使用 DashSet,这是一个并发哈希集的实现,允许并发内部化多个字符串,而不会成为瓶颈。

鉴于 IString 的特性,最好将字符串内部化一次并重复使用,而不是不断地从 &str 转换为 IString

IArray

IArray 类型类似于 Vec<IValue>。主要区别在于长度和容量存储在堆分配中,因此 IArray 本身可以是一个单个指针。

IObject

IObject 类型类似于 HashMap<IString, IValue>。与 IArray 一样,长度和容量存储在堆分配中。此外,IObject 保留了元素的插入顺序,以防原始 JSON 中这一点很重要。

IObject 中删除内容将破坏插入顺序。

技术细节

IValue

六个 JSON 类型分为具有较小固定值集的类型

  • null
  • bool

以及没有这些类型的

  • number
  • string
  • array
  • object

方便的是,这意味着我们只需要区分四种不同的堆分配类型(没有这些类型),这可以使用仅 2 位来完成。

我们确保我们的堆分配至少有 4 的对齐(这通常是默认的),这为我们留下了指针的两个最低位来存储一个 "tag" 值。

作为一个额外的优势,4 的对齐意味着有 3 个常数指针值(除了空指针)永远不会从 alloc 返回。

  • 0x1
  • 0x2
  • 0x3

这三个指针值与固定值 nullfalsetrue 分别对应。这样,我们就涵盖了所有可能的 JSON 类型!

剩下的就是找到一种方法,将数字、字符串、数组和对象存储在薄的指针后面。

INumber

在 JSON 中存储字节数组并不少见。如果我们需要为该数组中的每个字节进行堆分配,这将非常低效。此外,一些数字比其他数字更常见(0、1、-1)。

因此,我们需要一种方法来更高效地编码数字,使其越小越好,理想情况下,在不进行堆分配的情况下编码所有可能的字节值。但我们只有一个指针可用,并且我们已经使用了标签位!

好消息是字节值并不多(确切地说是 256 个),即使我们将范围扩展到有符号字节,也只有 384 个。我们可以在二进制中简单地预留一个静态数组来存储这些小的整数。

实际上,我们使用一个512项的数组来存储从 -128383 的值,这足以覆盖字节值范围。当我们需要存储这些数字之一时,我们只需将我们的指针设置为该数组中的适当条目,并跳过任何分配或释放。

IString

如前所述,字符串是内部化的。这不仅可以在对象数组中多次重复键时节省大量内存,而且还使得使用单个指针存储字符串变得非常简单,我们只需将指针设置为内部化字符串的位置。

我们还使用与数字类似的方法以更低的成本存储空字符串:我们只需声明一个静态变量作为空字符串,并使用指向它的指针。

IArray

这就像一个 Vec,但我们为分配的开头预留额外的空间来存储长度和容量。

我们再次使用静态变量优化,这样空的 Vec 就不需要分配。

IObject

IArray 相同的想法,同样的静态变量优化。

在分配内部,我们实际上存储了两个数组:第一个是一个简单的 IValue 数组,第二个是哈希表本身。哈希表仅存储第一个数组中的索引。

这简化了哈希表的实现,同时也允许我们保留插入顺序,并使迭代非常便宜(因为我们不需要跳过空条目)。

新值总是推送到数组的末尾,然后再将其索引插入到哈希表中。删除的值首先被交换到数组的末尾(请参阅 Vec::swap_remove),这样删除仍然可以在常数时间内完成。

由于键(IString)是内部化的,因此不需要存储哈希值,哈希函数可以是一个非常快速的运算,只需查看指针值。

依赖关系