#unicode-characters #unicode #id #code-point #no-alloc #programming-language

no-std unicode-id-start

根据Unicode标准附件#31确定字符是否具有ID_Start或ID_Continue属性

8个稳定版本

1.2.0 2024年7月8日
1.1.2 2023年9月13日
1.1.0 2023年3月8日
1.0.4 2024年5月9日
1.0.0 2022年5月20日

28过程宏

Download history 19653/week @ 2024-04-28 24888/week @ 2024-05-05 27212/week @ 2024-05-12 21682/week @ 2024-05-19 32489/week @ 2024-05-26 89060/week @ 2024-06-02 96332/week @ 2024-06-09 105583/week @ 2024-06-16 115305/week @ 2024-06-23 111945/week @ 2024-06-30 107137/week @ 2024-07-07 105720/week @ 2024-07-14 97662/week @ 2024-07-21 105739/week @ 2024-07-28 97045/week @ 2024-08-04 80477/week @ 2024-08-11

386,455 每月下载量
用于 294 个crate中(直接使用5个)

(MIT OR Apache-2.0) AND Unicode-DFS-2016

93KB
670

[!IMPORTANT] 此crate是较老 unicode-id crate的一个优化更好的实现。此crate使用更少的静态存储,并且能够以更好的性能对ASCII和非ASCII码点进行分类,比 unicode-id 快2-10倍。

Unicode ID_start

github crates.io docs.rs build status

Unicode标准附件#31 的实现,用于确定哪些 char 值是编程语言标识符中有效的。

变更日志

1.2.0

  • 修补 U+30FB KATAKANA MIDDLE DOT 和 U+FF65 HALFWIDTH KATAKANA MIDDLE DOT

Unicode 4.1至Unicode 15意外地将这两个字符从ID_Continue中省略。然而,这个错误在Unicode 15.1中得到纠正。任何支持ES6+但使用Unicode版本早于15.1的JS VM都将认为这是语法错误,因此我们故意省略这些字符,使其在ES5和ES6+中都是有效的标识符。更多信息请见 https://www.unicode.org/L2/L2023/23160-utc176-properties-recs.pdf 中的2.2节

1.1.2

  • Unicode 15.1.0

1.1.1

  • Unicode 15.0.0

1.0.4

  • Unicode 14.0.0

性能比较

以下表格显示了五种Unicode标识符实现的比较。

“静态存储”列显示了该包烘焙到您的二进制文件中的 static 表的总大小,单位为千字节。

剩余的列显示了评估单个 char 是否具有 ID_Start 或 ID_Continue Unicode 属性的 每次调用成本,比较了输入数据中 ASCII 到非 ASCII 代码点的不同比率。

static storage 0% nonascii 1% 10% 100% nonascii
unicode-ident 10.0 K 0.96 ns 0.95 ns 1.09 ns 1.55 ns
unicode-xid 11.5 K 1.88 ns 2.14 ns 3.48 ns 15.63 ns
ucd-trie 10.2 K 1.29 ns 1.28 ns 1.36 ns 2.15 ns
fst 138 K 55.1 ns 54.9 ns 53.2 ns 28.5 ns
roaring 66.1 K 2.78 ns 3.09 ns 3.37 ns 4.70 ns

基准测试的源代码位于本存储库的 bench 目录中,可以通过运行 cargo criterion 来重复。


数据结构比较

unicode-id

它们使用字符范围的有序数组,并执行二分搜索以查找给定字符是否落在这些范围之一内。

static ID_Continue_table: [(char, char); 763] = [
    ('\u{30}', '\u{39}'),  // 0-9
    ('\u{41}', '\u{5a}'),  // A-Z('\u{e0100}', '\u{e01ef}'),
];

此数据结构使用的静态存储量随着 Unicode 标识符代码点连续范围的数量的增加而增加。每个表条目消耗 8 字节,因为它由一对 32 位 char 值组成。

在 Unicode 代码点空间的某些范围内,这是一种相当稀疏的表示——存在一些范围,其中成千上万的相邻代码点都是有效的标识符字符。在其他地方,表示效率相当低。例如,像 µ (U+00B5) 这样的字符,周围是非标识符代码点,在表中消耗 64 位,而在密集位图中只需 1 位。

在一个 64 字节缓存行大小的系统上,对表进行二分搜索平均触及 7 个缓存行。每个缓存行只能容纳 8 个表条目。此外,二分搜索期间执行的分支很可能是对分支预测器不可预测的。

总的来说,与非 ASCII 输入相比,该包比最快的包慢大约 10 倍。

一个潜在的改进是将表条目更紧凑地打包。Rust 的 char 类型是一个 21 位整数,填充到 32 位,这意味着每个表条目都持有 22 位冗余空间,总共 3.9 K。它们可以将每个表条目压缩到 6 个字节,省去一些填充,从而在空间使用上提高 25%。通过一些巧妙的方法,可能可以将每个条目压缩到 5 个字节甚至 4 个字节,通过存储低 char 和范围而不是低 char 和高 char。我不期望性能会有很大提高,但这可能是在所有库中最有效的空间利用率,只需要大约 7 K 来存储。

ucd-trie

他们的数据结构是一个针对 Unicode 代码点专门设计的压缩 trie 集合。设计归功于 Raph Levien,在 rust-lang/rust#33098

pub struct TrieSet {
    tree1_level1: &'static [u64; 32],
    tree2_level1: &'static [u8; 992],
    tree2_level2: &'static [u64],
    tree3_level1: &'static [u8; 256],
    tree3_level2: &'static [u8],
    tree3_level3: &'static [u64],
}

它使用 trie 结构来表示代码点集合,并实现前缀压缩。trie 的最终状态嵌入在叶子或“块”中,每个块是一个 64 位整数。整数的每个位位置对应于特定的代码点是否在集合中。这些块不仅是 trie 最终状态的紧凑表示,也是一种后缀压缩形式。特别是,如果多个 64 个连续代码点的范围具有相同的 Unicode 属性,那么它们都将映射到 trie 最终层中的相同块。

由于针对 Unicode 代码点进行优化,这个 trie 被划分为三个不相交的集合:tree1、tree2、tree3。第一个集合对应于代码点 [0, 0x800),第二个 [0x800, 0x10000),第三个 [0x10000, 0x110000)。这些划分方便地对应于 1 或 2 字节 UTF-8 编码的代码点空间、3 字节 UTF-8 编码的代码点空间和 4 字节 UTF-8 编码的代码点空间。

在此数据结构中的查找效率比二分搜索高得多。查找操作基于访问哪个 trie 分区,可能会触及 1、2 或 3 个缓存行。

一种可能的性能改进是为此 crate 提供一种基于 UTF-8 编码字符串进行查询的方式,返回字符串中第一个字符对应的 Unicode 属性。如果没有这样的 API,调用者必须将他们的 UTF-8 编码输入数据拆分为 char,然后将 char 交给 ucd-trie,而 ucd-trie 会通过将其转换回 trie 遍历的变量长度表示来撤销这项工作。

fst

使用了一个 有限状态转换器。这种表示集成在 ucd-generate 中,但我不知道它与 ucd-trie 表示相比有任何优势。特别是 ucd-trie 优化用于存储 Unicode 属性,而 fst 则不是。

据我所知,与 ucd-trie 相比,导致 fst 在此用例中具有大尺寸和慢速查找的主要原因是没有针对只有 21 个 char 中的 32 位是有意义的这一事实进行优化。在结构中存在一些密集的数组,这些数组中有大范围的值永远不可能被使用。

roaring

此 crate 是 Roaring Bitmap 的纯 Rust 实现,这是一个用于存储 32 位无符号整数集合的数据结构。

Roaring 位图是压缩位图,通常比传统的压缩位图(如 WAH、EWAH 或 Concise)表现更好。在某些情况下,它们可以快数百倍,并且通常提供显著的压缩。

在此用例中,性能相当有竞争力,但仍然比 Unicode 优化 crate 慢得多。同时,压缩显著更差,需要为数据结构分配 6 倍的存储空间。

我还对 croaring crate 进行了基准测试,这是一个围绕 C 引用实现 Roaring Bitmap 的 FFI 包装器。这个 crate 比纯 Rust 的 roaring 慢约 15%,这可能是 FFI 开销。我没有进一步调查。

unicode-ident

此 crate 与 ucd-trie 库最相似,因为它基于存储在 trie 表示叶子中的位图,实现了前缀压缩和后缀压缩。

主要区别在于

  • 使用单个 2 级 trie,而不是不同深度的三个不相交分区。
  • 使用更大的块:512 位而不是 64 位。
  • 同时压缩ID_Start和ID_Continue属性,而不是在两个中重复相同的trie叶子块。

以下图显示了未压缩的ID_Start和ID_Continue Unicode布尔属性,按行主序排列

ID_StartID_Continue
ID_Start bitmap ID_Continue bitmap

未压缩时,这些属性需要140 K的存储空间,这超出了合理范围。然而,正如您所看到的,这两个位图以及行之间存在很大的相似度,这非常适合压缩。

该crate在trie的叶子级别存储上述位图的512位“行”,并增加一个单独的级别来索引叶子。结果发现,在两个位图之间有124个独特的512位块,因此7位足够索引它们。

选择512位块大小作为使数据结构总大小最小化的尺寸。较小的块,如256或128位,会实现更好的去重,但需要更大的索引。较大的块会增加叶子位图中的冗余。512位块是索引总大小和叶子位图的最佳选择。

实际上,由于只有124个独特的块,我们可以使用8位索引,并保留一个位来在半块级别进行索引。这通过消除任何块的第二半与其他块的任何第一半之间的冗余,实现了额外的8.5%压缩。请注意,这不同于使用大小减半的块,因为它不需要增加trie的第一级大小。

与二分搜索或ucd-trie crate相比,在这个数据结构中进行查找是直线代码,无需分支。

is_id_start:
	mov eax, edi
	shr eax, 9
	lea rcx, [rip + unicode_ident::tables::TRIE_START]
	add rcx, rax
	xor eax, eax
	cmp edi, 201728
	cmovb rax, rcx
	test rax, rax
	lea rcx, [rip + .L__unnamed_1]
	cmovne rcx, rax
	movzx eax, byte ptr [rcx]
	shl rax, 5
	mov ecx, edi
	shr ecx, 3
	and ecx, 63
	add rcx, rax
	lea rax, [rip + unicode_ident::tables::LEAF]
	mov al, byte ptr [rax + rcx]
	and dil, 7
	mov ecx, edi
	shr al, cl
	and al, 1
	ret

许可证

使用Unicode字符数据库,本crate受Unicode许可证协议 – 数据文件和软件(2016)的约束。

本crate中所有非使用Unicode字符数据库作为输入生成的知识产权,根据您的选择,受Apache许可证,版本2.0MIT许可证的约束。

生成的文件结合了来自Unicode字符数据库的表格数据,以及crate原始源代码内容的知识产权。当涉及这些生成文件时,必须遵守Unicode许可证协议以及Apache许可证或MIT许可证的条款。

除非您明确声明,否则您提交的任何有意包含在本crate中的贡献,如Apache-2.0许可证中定义的,应按上述方式许可,而无需任何额外条款或条件。

无运行时依赖项