请解释struct与class在栈/堆分配上的差异

6 阅读3分钟

解读

国内Unity面试中,这道题出现的频率极高,面试官真正想考察的是“值类型与引用类型的内存模型差异”以及“你是否能在性能敏感场景下正确选型”
很多候选人只回答“struct在栈、class在堆”,结果直接被追问“数组呢?闭包呢?IL2CPP下呢?”——如果答不上来,会被判定为“只背了八股文”。
因此,答案必须把CLI规范、Unity实际内存布局、IL2CPP后端行为、Burst限制四层信息逐层展开,体现“工程落地能力”。

知识点

  1. CLI规范层面:值类型与引用类型的抽象定义,与具体运行时无关。

  2. IL2CPP转译层面:Unity把C#转C++,栈/堆分配决策权在C++编译器,不再由CLR托管。

  3. Burst编译器层面:Burst要求所有值类型必须是Blittable无托管引用,否则回退到托管堆。

  4. Unity Profiler验证层面:用Memory Profiler或Deep Profile可直接看到struct被内联到Native Memory或C++栈,而非Mono堆。

  5. 性能陷阱

    • 值类型数组无论元素是struct还是class,数组本体都在托管堆,元素布局遵循“引用压缩”或“顺序内联”。
    • 闭包捕获的局部struct会被装箱到托管堆,因为编译器生成隐藏class。
    • 协程yield return struct同样会装箱。

答案

纯.NET Framework/Mono运行时下,简要结论:

  • struct是值类型,未装箱时随所在上下文分配:局部变量随栈帧弹出,字段则内嵌于宿主对象(类或数组)的内存区域。
  • class是引用类型,对象本体永远在托管堆,栈上仅存指针。

但在Unity的IL2CPP后端中,上述“栈/堆”概念被重新映射:

  1. 局部struct若未被闭包捕获,IL2CPP将其生成C++的栈变量,生命周期随C++栈帧销毁,无GC压力
  2. 局部class变量,IL2CPP生成Object_t*指针,对象本体在IL2CPP托管堆(仍受GC管理)。
  3. struct数组虽在托管堆,但元素顺序紧密排列,遍历缓存友好;class数组元素是指针数组,多一次解引用且随机访问,Cache Miss更高
  4. 若struct被装箱(如接口调用、委托、协程),IL2CPP会生成System_ValueType*指针,对象本体进入托管堆,与class无差异。

因此,不能简单背“struct栈、class堆” ,而应回答:
“在Unity IL2CPP下,struct默认随C++栈帧分配,生命周期可预测;class对象始终在托管堆。但数组、闭包、接口装箱都会让struct进入堆,需用Profiler验证。”

拓展思考

  1. Job System与Burst
    只有** unmanaged 的struct**(无托管字段)才能被Burst编译为SIMD指令,并完全分配在Native Memory;一旦混有stringobject,编译失败或回退托管堆。
  2. 移动端GC卡顿实战
    某MMO项目把1000个MonsterData从class改为struct,并改用NativeArray<MonsterData>每帧GC.Alloc从1.2 MB降到0,Android低端机卡顿帧率提升8 FPS。
  3. 面试反杀技巧
    如果面试官追问“如何证明struct在栈”,可回答:
    “用Unity 2021.3 Memory Profiler抓帧,勾选‘Native Memory’视图,能看到局部struct变量出现在PlayerMainThreadStack区域,而class对象出现在IL2CPP Managed Heap区域,直接给出截图即可让面试官闭嘴。”