在编程中因为缺乏对内存布局的了解,我们就浪费了如此多的内存,这真是太令人抓狂了。让我们回顾一下编程语言中一些基本类型的内存大小(以Rust为例):
bool : 1
u8 : 1
u16 : 2
u32 : 4
u64 : 8
char : 4
既然我们知道每个基本类型的字节大小,那么问一个问题。以下结构体的字节大小是多少?
struct Foo {
elem: u32,
other: u16,
}
如果是6,就不会问你这个问题了。虽然6并不是完全不正确——因为结构体对于u32包含4个字节,对于u16包含2个字节——结构体的大小是通过对齐最大的字段并将总大小四舍五入到最接近的倍数来计算的。如果这不是很清楚,让我们看一个简单的表示。
现在让我们猜测下面结构体的内存大小(以字节为单位):
struct Bar {
num: u16,
bigger: u32,
another: u16,
}
如果我们遵循前面描述的心智模型,Bar结构体的大小将是12。如果我们考虑结构体的字段的最大对齐,即4,并将所有其他字段的大小四舍五入为4,我们得到:4 4 4 = 12。
结果如下:(2 + 2) + 4 = 8。似乎我们通过重新排列结构体的字段优化了空闲空间,事实上,Rust编译器足够聪明,可以自动完成这项工作,但并不是所有强类型语言都是如此!让我们打印Rust结构体的内存大小,然后对C中的等效结构体做同样的事情:
Rust结构体在内存中的大小:
std::mem::size_of::<Bar>() // output: 8
C结构体在内存中的大小:
struct Bar {
uint16_t num;
uint32_t bigger;
uint16_t another;
};
sizeof(struct Bar) // output: 12
似乎Rust编译器优化了结构体,而C编译器没有!这是因为C程序员真的需要理解计算机是如何在底层(ABI)工作的,不像Rust程序员(只是开玩笑😳)…更严重的是,Rust编译器是高度优化的,并为你处理这些,但在C中简单的手动重新排列将达到相同的结果。Rust编译器会高度优化你指示它优化的任务。这并不意味着在Rust中你可以忽略数据布局,理解和考虑数据布局仍然很重要。
下面是当前结构体数据布局的可视化表示:
现在,让我们重新排序C结构体的字段并打印它的大小:
struct Bar {
uint32_t bigger;
uint16_t num;
uint16_t another;
};
sizeof(struct Bar) // output: 8
让我们继续探索枚举的数据布局,你知道Rust中枚举的大小吗?
enum HtmlTag {
H1,
H2,
UnorderedList,
OrderedList,
...
}
std::mem::size_of::<HtmlTag>() // output: 1
请注意,我们所讨论的枚举除了每个字段的区别外不携带任何数据。在本例中,此类枚举的大小为1字节。现在,假设我们正在构建一个HTML标记器—一个循环遍历HTML源代码文件并提取文件中所有特定HTML标记及其位置的程序。简化结构如下:
struct HtmlToken {
start_position: u32,
token_tag: HtmlTag,
}
std::mem::size_of::<HtmlToken>() // output: 8
该结构体的大小是8字节!u32为4字节,enum为1字节,填充为3字节。现在,假设我们对一个大型HTML网页进行标记,并生成10,000个标记。在这种情况下,内存中总大小为10,000 * 8 = 80,000字节,其中填充30,000字节🥵。这是对内存的巨大浪费,而且这种情况比你想象的要频繁得多。布尔值也有1字节的大小。例如,如果你有一个带有整数和布尔值的结构体,它可能会为该结构体的每个实例生成3个字节的填充。
在本例中,解决方案是将枚举存储在带外。我们将使用由多个数组组成的数据结构。这意味着我们将使用两个数组并使用索引并行访问它们。在这种设计下,我们不会为每个新实例生成填充:
struct HtmlTokens {
start_positions: [u32; 1],
token_tags: [HtmlTag; 1],
}
std::mem::size_of::<HtmlTokens>() // output: 5
使用多数组技术,我们将每个HTML令牌的8字节实例转换为两个数组,每个数组只包含5字节的实例!以我们的HTML标记器为例,这种方法将内存中的标记列表大小减少了40%……对于这样简单的修改来说,这真是令人印象深刻。
总结
在这篇文章中介绍了两种技术来帮助你优化结构体内存大小:结构体中字段的顺序可以显著影响数据布局和编译器优化;将布尔值和1字节枚举存储在带外,以避免不必要的填充。