静态数据繁重的 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 为我指出解决方案。这是问题更新的总结,抱歉重复!

看来所谓的膨胀有两个原因:

  1. 输出的.rlib文件不是纯目标文件,而是ar归档文件。通常这样的文件将完全由一个或多个目标文件组成,但 Rust 还包括元数据。这样做的部分原因似乎是为了避免需要单独的头文件。这占最终文件大小的 40% 左右。

  2. 我的计算对于一些表来说并不准确,而这些表恰好也是最大的。使用 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 兆字节的膨胀,但似乎占了整个文件的大小。