资源捆绑字符串

问题

字符串值是资源束中数据的很大一部分。ICU数据总体上很大,部分原因是我们在资源束字符串中有大量字节。

当前存储格式(高达ICU 4.2)

字符串的存储方式可以最大限度地提高运行时性能,但它们并不紧凑。

所有资源项都有一个32位字,其中包含一个4位数据类型字段和一个28位值字段。除了直接存储在此资源项字中的单个28位整数外,value字段包含实际资源存储位置的偏移量。偏移量左移2,所有资源至少对齐4字节边界。

字符串资源具有int32_t长度、UTF-16字符串内容、NUL终止符(1个UChar=2个字节),并且与所有资源一样,有时也会进行填充以获得4个字节的倍数。

一个空的或一个UChar字符串总共占用12个字节:资源项中4个字节(类型为&offset),长度为4个字节,内容为0或2,NUL为2,填充为0或2。

2..3个UChars=16字节,4..5个UChar=20字节等。

低垂水果(在ICU 4.4中实施)

我们应该消除重复字符串(只存储一个副本。)从原型来看,这将节省大约700kB。

更紧凑的UTF-16(在ICU 4.4中实施)

我们可以存储相对较短的UTF-16字符串没有长度整数,并且仅使用NUL终止符。较长的字符串将以一个、两个或三个16位单位作为长度的前缀(第一个单位位于特殊范围内,例如,trail surrate)。我们可以在包的早期将所有这样的字符串存储在一个公共块中,这样就不需要每次字符串填充。

这需要一个新的字符串资源类型,但可以使用内存映射,不需要任何堆分配。检索短字符串需要调用u_strlen()。我们希望定义“short”,这样可以节省大量空间,同时保持u_strlen()的平均时间较短。“short”的合理截止长度可能介于16到64个代码单位之间。这个截止值将由genrb设置,运行时代码不需要知道也不依赖它。(也就是说,我们可以在不更改数据格式的情况下更改截止值,即使是作为命令行选项。)

如果我们可以避免存储140000个字符串中大多数字符串的长度(4字节整数),并在填充中平均每个字符串保存1字节,那么这将节省大约700kB。

此外,前面没有长度值的字符串可以使用后缀共享。(请参见 按键页面.)

我们应该重新介绍空字符串作为0的资源项(UTF-16字符串类型,偏移量为0)。这是原始设计的一部分,但在实现中被忽略了。如果有机会使用新的formatVersion,我们应该重新使用它。例如,它在存储res_index.txt文件(所有值都为空字符串的键值对)的方式上有一点不同,但成本很低。

简单压缩(从ICU 4.4开始未实施)

对于大多数脚本,两种简单的技术可以使每个字符接近1个字节,对于CJK,每个字符接近2个字节,与当前格式相比,开销更少(长度字节更少,没有终止符,没有填充)。它可能比SCSU简单得多。(除非我们从根本上重新考虑资源包数据格式,否则仍需要应用4字节填充。)

    • 在每个捆绑包中,确定包含最多字符数的3个SCSU样式“窗口”(128个字符范围)。

    • 可以使用一种新的资源类型直接在资源项字中存储“微型字符串”,而不是偏移量:0..4个ASCII字符、2..3个同窗口字符或一个任意代码点。

    • 另一种新的资源类型可以包含一个字节字符串,该字符串的长度以可变长度编码,后跟压缩的字符串字节。第一个长度字节将有2位用于压缩类型。类型0..2表示单字节模式,初始状态设置为三个SCSU样式窗口之一,ASCII和窗口字符以单字节表示。类型3是CJK模式,直接编码主要的Han和Hangul块,每个ASCII字符一个字节。

    • 所有压缩的非迷你字符串都将存储在包的开头,就在键之后,以避免出现伪字符串填充。

此实现的修补程序附加到此页面。它只计算压缩长度,但实际上并不以压缩形式写入字符串。

在资源束头中,我们可能会存储压缩字符串的数量(1 int)、它们将解压缩到的UChar的数量(包括长度为1或2,NUL为1;1 int),以及三个窗口开始代码点(3*uint16_t,省略每个窗口的底部7位)。

将有一些未使用的字节组合。我们可能希望将压缩方案的版本号存储在.resformatVersion之外的几个标头位中。

从稍微修改过的svn r25919生成的几乎是ICU4C 4.2版本的结果:

.dat文件大小:16011616字节

资源束数量:660

不带标头的总大小:10665468字节

字符串数:140202

包含资源项的字符串大小:6589592字节

大小缩减:2942546(头中减去约660*16=10560字节)

压缩字符串后的总填充:570字节

适合迷你格式的字符串数:25033

节省:297624

14个字符串不可压缩(“压缩”形式比原始UTF-16更长)

节省约为.dat文件大小的18.4%

通用压缩(自ICU 4.4起未实施)

我们应该考虑对很长、很难压缩的字符串进行通用压缩,至少是很少访问的字符串,比如CJK排序规则。请参阅 通用压缩页面进行一般性讨论。

我们可能会使用与更简单的压缩方案相同的新字符串资源类型,并在压缩字符串的第一个字节中进行区分。我们希望在压缩字节之前存储UTF-16长度(用于分配解压缩字符串)和字节数(传递给解压缩器)。

压缩的运行时问题

ICU4C API返回一个字符串作为指向以NUL结尾的UChar的指针,该UChar以当前格式存储。如果我们以任何方式压缩字符串,那么我们需要在运行时解压缩并缓存它们,直到释放资源包。解压缩和缓存需要同步(互斥)。

    • 如果我们在加载资源包时解压缩所有字符串,那么我们会在许多永远无法访问的字符串上浪费时间和堆空间。

    • 如果我们在第一次访问每个字符串时解压缩它,那么我们需要在getString()函数中进行同步。

    • 作为一种折衷方案,在访问字符串的包含数组或表时解压缩字符串可能是合理的。除了排序数据之外,访问二进制数据是非常常见的,访问规则字符串(可以很大)的情况非常少见,而且它们位于同一个表中。

    • 我们可能需要使用资源项单词作为键(或类似键)来维护一个哈希表。每数组/每表解压缩可能会稍微简化这一点,因为数组和表通常包含密集的字符串资源列表,或者根本没有字符串。

对于增量解压缩,我们应该尝试进行增量内存分配,以便在很少访问字符串时只使用少量堆内存。

如果字符串是通过不简单返回const UChar*指针的API访问的,例如ures_getUTF8String()和ures_getUnicodeString(。

可能的添加和替代

我们应该看看别名资源项当前存储方式与UTF-16中的字符串类似。它们需要使用Unicode吗?它们可以像键一样使用不变字符(char*)吗?还是ASCII或UTF-8格式?(但它们相对较少且较小。)

如果我们用NUL结束符而不是长度来存储压缩字符串,那么我们可以使用后缀共享。(请参阅 按键页面.)

    • 我们必须在其他地方存储首字母window/CJK状态的2位。例如,从资源项中再取2位(将偏移量从28位减少到26位,从而将最大字符串字节数从256MB减少到64MB),或者取4个字符串类型而不是1。

    • 我们必须修改编码,以避免所有多字节字符编码的尾字节中的终止字节。

    • 在运行时,我们必须预先飞行长度;对于短字符串,我们可以解码到堆栈缓冲区中,并将解码后的字符串复制到缓存中。对于长字符串,我们可能会使用纯预照明并读取压缩的字节两次。

我们可以将所有“容器”类型(表和数组)存储在一个连续的块中,最容易的方法是在包的末尾。在运行时,我们可以将此部分复制到堆内存中,当我们解压缩字符串并将其存储在缓存中时,可以在堆副本中修改其资源项字,以便下次看到标准UTF-16字符串资源类型。我们可能必须小心处理某些CPU上的内存排序问题。(也就是说,对堆表/数组中资源项的访问可能必须由UMTX_CHECK()保护。)