元组与数组在栈上的内存布局区别?

解读

国内大厂(字节、阿里、华为)的 Rust 岗一面,90% 会追问“栈内存长什么样”。面试官真正想确认的是:

  1. 你是否把元组当成“字段顺序固定的匿名 struct”,把数组当成“同类型连续块”;
  2. 你是否知道 Rust 编译器会对元组做字段级对齐,而数组只做元素对齐
  3. 你是否能口算出 size_of::<(T, U)>()size_of::<[T; N]>() 的差异,并解释 padding 来源。
    答不出“对齐 + 顺序 + 连续”三件套,基本会被扣“只写过业务、没写过底层”的帽子。

知识点

  1. 元组
    • 异构、定长、字段名被编译器编号(0,1,2…)
    • 内存顺序 = 声明顺序,编译器按 Rust 平台默认对齐规则 在字段间插入 padding
    • 整体对齐 = 字段最大 align 值,整体大小是 最后一个字段偏移 + 最后一个字段大小 + 尾部 padding
  2. 数组
    • 同构、定长、元素紧密排列
    • 内存布局 = 单元素布局 × N,元素间 无额外 padding(仅每个元素内部可能有自身对齐需要的 padding)
    • 整体对齐 = 单元素对齐值,整体大小恒为 size_of::<T>() × N
  3. 验证工具
    • std::mem::{size_of, align_of}
    • #[repr(Rust)](默认)与 #[repr(C)] 均可打印,但 repr(Rust) 允许字段重排,当前实现仍按声明顺序;面试时直接说“当前实现顺序固定,未来可能重排”显得严谨
  4. 栈分配
    • 两者均 直接在栈帧上展开无堆指针、无胖指针;长度信息对数组是编译期常量,对元组是类型系统常量,运行时均不携带额外字段

答案

“在 64-bit Linux 上,元组是异构字段顺序排列,数组是同构元素紧密排列”。
举例:

let t: (u8, u32, u8) = (0, 1, 2);
let a: [u16; 3] = [0, 1, 2];
  • size_of::<(u8, u32, u8)>() == 12
    字段 0 偏移 0 占 1 字节,pad 3 字节;
    字段 1 偏移 4 占 4 字节;
    字段 2 偏移 8 占 1 字节,尾部 pad 3 字节;
    整体对齐 4。
  • size_of::<[u16; 3]>() == 6
    每个 u16 对齐 2,元素紧密排布,无中间 padding,整体对齐 2。
    结论:元组可能有内部与尾部 padding,数组只有元素自身对齐,无额外 padding;二者都直接在栈上展开,不携带运行时长度字段

拓展思考

  1. 如果把元组改成 #[repr(C)],布局仍与默认一致吗?
    答:当前一致,但 repr(C) 禁止 Rust 未来重排,跨 FFI 更安全;面试可补充“我用 repr(C) 做 ABI 绑定”。
  2. 把数组放进元组 ([u8; 3], u32),整体大小是多少?
    答:数组内部无 pad,但元组字段间需对齐 u32,所以是 3 + 1(pad) + 4 = 8 字节
  3. 为什么 Vec<T> 不在栈上?
    答:Vec 是胖指针(ptr, cap, len)三字段结构体,实际数据在堆;追问时可顺口带出 SmallVec<[T; N]>ArrayVec 做栈上变长优化,展示你对生态的熟悉度。