静态数据繁重的 Rust 库似乎臃肿
Static data-heavy Rust library seems bloated
我最近一直在开发一个 Rust 库,以尝试提供对大型数据库的快速访问(Unicode 字符数据库,作为一个平面 XML 文件是 160MB)。我还希望它占地面积小,因此我使用了各种方法来减小尺寸。最终结果是我有一系列静态切片,如下所示:
#[derive(Clone,Copy,Eq,PartialEq,Debug)]
pub enum UnicodeCategory {
UppercaseLetter,
LowercaseLetter,
TitlecaseLetter,
ModifierLetter,
OtherLetter,
NonspacingMark,
SpacingMark,
EnclosingMark,
DecimalNumber,
// ...
}
pub static UCD_CAT: &'static [((u8, u8, u8), (u8, u8, u8), UnicodeCategory)] =
&[((0, 0, 0), (0, 0, 31), UnicodeCategory::Control),
((0, 0, 32), (0, 0, 32), UnicodeCategory::SpaceSeparator),
((0, 0, 33), (0, 0, 35), UnicodeCategory::OtherPunctuation),
/* ... */];
// ...
pub static UCD_DECOMP_MAP: &'static [((u8, u8, u8), &'static [(u8, u8, u8)])] =
&[((0, 0, 160), &[(0, 0, 32)]),
((0, 0, 168), &[(0, 0, 32), (0, 3, 8)]),
((0, 0, 170), &[(0, 0, 97)]),
((0, 0, 175), &[(0, 0, 32), (0, 3, 4)]),
((0, 0, 178), &[(0, 0, 50)]),
/* ... */];
总的来说,所有数据最多应该只占用大约 600kB(假设额外的 space 用于对齐等),但生成的库在发布模式下为 3.3MB。源代码本身(几乎所有数据)是 2.6MB,所以我不明白为什么结果会更多。我不认为额外的大小是固有的,因为在项目开始时大小小于 50kB(当时我只有 ~2kB 的数据)。如果它有所作为,我也在使用 #![no_std]
功能。
是否有任何额外的二进制膨胀的原因,有没有办法减少大小?理论上我不明白为什么我不能将库减少到 1 MB 或更少。
根据 Matthieu 的建议,我尝试用 nm
.
分析二进制文件
因为我所有的 table 都表示为借来的切片,所以这对于计算 table 大小不是很有用,因为它们都是匿名的 _ref
。我可以确定的是最大地址 0x1208f8,这与 ~1MB 而不是 3.3MB 的文件大小一致。我还查看了十六进制转储,看看是否有任何空块可以解释它,但没有。
为了查看是否是借用的切片有问题,我将它们变成了非借用的切片([T; N]
形式)。文件大小没有太大变化,但现在我可以很容易地解释 nm
数据。奇怪的是,tables 正好占据了我的预期(更奇怪的是,在不考虑对齐的情况下,它们与我的下限相匹配,并且 table 之间没有 space s).
我还查看了带有嵌套借用切片的 tables,例如UCD_DECOMP_MAP
以上。当我删除所有这些(大约 2/3 的数据)时,文件大小为 ~1MB,而它应该只有 ~250kB(根据我的计算和最高 nm
地址,0x3d1d0),所以它没有'看起来这些 table 也不是问题所在。
我尝试从 .rlib 文件(这是一个简单的 ar 格式存档)中提取单个文件。事实证明,该库的 40% 只是元数据文件,实际目标文件为 1.9MB。此外,当我在没有借用引用的情况下对库执行此操作时,目标文件为 261kB!然后我回到原来的库,查看单个 _ref
的大小,发现对于像 UCD_DECOMP_MAP: &'static [((u8,u8,u8),&'static [(u8,u8,u8)])]
这样的 table,每个 ((u8,u8,u8),&'static [(u8,u8,u8)])
类型的值占用24 个字节(3 个字节用于 u8 三元组,5 个字节的填充和 16 个字节的指针),因此这些 table 占用的空间比我想象的要多得多。我想我现在可以完全解释所有文件大小了。
当然,3MB还是很小的,我只是想文件越小越好!
感谢 Matthieu M. 和 Chris Emerson 为我指出解决方案。这是问题更新的总结,抱歉重复!
看来所谓的膨胀有两个原因:
输出的.rlib
文件不是纯目标文件,而是ar
归档文件。通常这样的文件将完全由一个或多个目标文件组成,但 Rust 还包括元数据。这样做的部分原因似乎是为了避免需要单独的头文件。这占最终文件大小的 40% 左右。
我的计算对于一些表来说并不准确,而这些表恰好也是最大的。使用 nm
我能够发现对于 UCD_CAT: &'static [((u8,u8,u8), (u8,u8,u8), UnicodeCategory)]
这样的普通表,每个项目的大小是 7 个字节(实际上比我最初预期的 少 ,假设对齐 8 个字节)。所有这些表的总和约为 230kB,仅包括这些表的目标文件的大小为 260kB(提取后),所以这一切都是一致的。
但是,更仔细地检查其他表(例如 UCD_DECOMP_MAP: &'static [((u8,u8,u8),&'static [(u8,u8,u8)])]
)的 nm
输出更加困难,因为它们显示为匿名借用对象。然而,事实证明每个 ((u8,u8,u8),&'static [(u8,u8,u8)])
实际上占用 24 个字节:第一个元组 3 个字节,填充 5 个字节,指针意外占用 16 个字节。我相信这是因为指针还包括引用数组的大小。这给库增加了大约 1 兆字节的膨胀,但似乎占了整个文件的大小。
我最近一直在开发一个 Rust 库,以尝试提供对大型数据库的快速访问(Unicode 字符数据库,作为一个平面 XML 文件是 160MB)。我还希望它占地面积小,因此我使用了各种方法来减小尺寸。最终结果是我有一系列静态切片,如下所示:
#[derive(Clone,Copy,Eq,PartialEq,Debug)]
pub enum UnicodeCategory {
UppercaseLetter,
LowercaseLetter,
TitlecaseLetter,
ModifierLetter,
OtherLetter,
NonspacingMark,
SpacingMark,
EnclosingMark,
DecimalNumber,
// ...
}
pub static UCD_CAT: &'static [((u8, u8, u8), (u8, u8, u8), UnicodeCategory)] =
&[((0, 0, 0), (0, 0, 31), UnicodeCategory::Control),
((0, 0, 32), (0, 0, 32), UnicodeCategory::SpaceSeparator),
((0, 0, 33), (0, 0, 35), UnicodeCategory::OtherPunctuation),
/* ... */];
// ...
pub static UCD_DECOMP_MAP: &'static [((u8, u8, u8), &'static [(u8, u8, u8)])] =
&[((0, 0, 160), &[(0, 0, 32)]),
((0, 0, 168), &[(0, 0, 32), (0, 3, 8)]),
((0, 0, 170), &[(0, 0, 97)]),
((0, 0, 175), &[(0, 0, 32), (0, 3, 4)]),
((0, 0, 178), &[(0, 0, 50)]),
/* ... */];
总的来说,所有数据最多应该只占用大约 600kB(假设额外的 space 用于对齐等),但生成的库在发布模式下为 3.3MB。源代码本身(几乎所有数据)是 2.6MB,所以我不明白为什么结果会更多。我不认为额外的大小是固有的,因为在项目开始时大小小于 50kB(当时我只有 ~2kB 的数据)。如果它有所作为,我也在使用 #![no_std]
功能。
是否有任何额外的二进制膨胀的原因,有没有办法减少大小?理论上我不明白为什么我不能将库减少到 1 MB 或更少。
根据 Matthieu 的建议,我尝试用 nm
.
因为我所有的 table 都表示为借来的切片,所以这对于计算 table 大小不是很有用,因为它们都是匿名的 _ref
。我可以确定的是最大地址 0x1208f8,这与 ~1MB 而不是 3.3MB 的文件大小一致。我还查看了十六进制转储,看看是否有任何空块可以解释它,但没有。
为了查看是否是借用的切片有问题,我将它们变成了非借用的切片([T; N]
形式)。文件大小没有太大变化,但现在我可以很容易地解释 nm
数据。奇怪的是,tables 正好占据了我的预期(更奇怪的是,在不考虑对齐的情况下,它们与我的下限相匹配,并且 table 之间没有 space s).
我还查看了带有嵌套借用切片的 tables,例如UCD_DECOMP_MAP
以上。当我删除所有这些(大约 2/3 的数据)时,文件大小为 ~1MB,而它应该只有 ~250kB(根据我的计算和最高 nm
地址,0x3d1d0),所以它没有'看起来这些 table 也不是问题所在。
我尝试从 .rlib 文件(这是一个简单的 ar 格式存档)中提取单个文件。事实证明,该库的 40% 只是元数据文件,实际目标文件为 1.9MB。此外,当我在没有借用引用的情况下对库执行此操作时,目标文件为 261kB!然后我回到原来的库,查看单个 _ref
的大小,发现对于像 UCD_DECOMP_MAP: &'static [((u8,u8,u8),&'static [(u8,u8,u8)])]
这样的 table,每个 ((u8,u8,u8),&'static [(u8,u8,u8)])
类型的值占用24 个字节(3 个字节用于 u8 三元组,5 个字节的填充和 16 个字节的指针),因此这些 table 占用的空间比我想象的要多得多。我想我现在可以完全解释所有文件大小了。
当然,3MB还是很小的,我只是想文件越小越好!
感谢 Matthieu M. 和 Chris Emerson 为我指出解决方案。这是问题更新的总结,抱歉重复!
看来所谓的膨胀有两个原因:
输出的
.rlib
文件不是纯目标文件,而是ar
归档文件。通常这样的文件将完全由一个或多个目标文件组成,但 Rust 还包括元数据。这样做的部分原因似乎是为了避免需要单独的头文件。这占最终文件大小的 40% 左右。我的计算对于一些表来说并不准确,而这些表恰好也是最大的。使用
nm
我能够发现对于UCD_CAT: &'static [((u8,u8,u8), (u8,u8,u8), UnicodeCategory)]
这样的普通表,每个项目的大小是 7 个字节(实际上比我最初预期的 少 ,假设对齐 8 个字节)。所有这些表的总和约为 230kB,仅包括这些表的目标文件的大小为 260kB(提取后),所以这一切都是一致的。但是,更仔细地检查其他表(例如
UCD_DECOMP_MAP: &'static [((u8,u8,u8),&'static [(u8,u8,u8)])]
)的nm
输出更加困难,因为它们显示为匿名借用对象。然而,事实证明每个((u8,u8,u8),&'static [(u8,u8,u8)])
实际上占用 24 个字节:第一个元组 3 个字节,填充 5 个字节,指针意外占用 16 个字节。我相信这是因为指针还包括引用数组的大小。这给库增加了大约 1 兆字节的膨胀,但似乎占了整个文件的大小。