BPVM 小食包 #18 - 为什么蓝图更慢:性能真相
蓝图比 C++ 慢,但不是你可能想的那些原因。不是虚拟机 - 而是复制!这是真实的性能故事。
BPVM 小食包 #18 - 为什么蓝图更慢:性能真相
本文内容基于Unreal Engine 5.6.0
BPVM 小食包 - 蓝图知识快速投喂!是蓝图到字节码系列的一部分。
性能问题
“为什么蓝图比 C++ 慢?”
你会听到的大多数答案都是错的。让我们破除一些神话!
神话 #1: “解释代码很慢”
错! 蓝图不是解释的 - 它被编译为字节码。
虚拟机非常高效地执行这个字节码。蓝图中的简单循环几乎和 C++ 一样快!
神话 #2: “可视化脚本有开销”
错! 可视化节点在编译时消失。
运行编译的蓝图有零可视化开销。那些节点只是编辑器表示!
真正的罪魁祸首:复制
这是真正的性能杀手:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C++(快)
void MyFunction(const FVector& Location) {
// 直接内存访问,无复制
UseLocation(Location);
}
// 蓝图(慢)
void MyFunction(FVector Location) {
// 步骤 1: 将 FVector 复制到参数栈(12 字节)
memcpy(ParamBuffer, &Location, sizeof(FVector));
// 步骤 2: 执行函数
// 步骤 3: 清理栈
// 总计: 约 100 纳秒的复制开销!
}
每个函数调用都复制数据!
复制开销
让我们测量一下:
1
2
3
4
5
6
7
8
// C++ 函数调用
MyFunc(Vector, Actor, String);
// 时间: 约 10 纳秒
// 蓝图函数调用
MyFunc(Vector, Actor, String);
// 时间: 约 50-100 纳秒
// 额外时间 = 复制参数!
蓝图仅从复制就慢 5-10 倍!
栈管理成本
虚拟机维护一个运行时栈:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C++(编译的栈管理)
void Call() {
int Local = 5; // 栈指针在编译时调整
}
// 蓝图(运行时栈管理)
void Call() {
// 虚拟机在运行时分配栈空间
uint8* Stack = AllocateStack(FunctionStackSize);
// 虚拟机管理局部变量
int* Local = (int*)(Stack + LocalOffset);
// 虚拟机清理
FreeStack(Stack);
}
运行时栈管理每次调用增加微秒!
类型检查开销
虚拟机进行运行时类型检查:
1
2
3
4
5
6
7
8
9
// C++(编译时,零成本)
AActor* MyActor = GetActor(); // 编译器验证类型
// 蓝图(运行时成本)
AActor* MyActor = GetActor();
// 虚拟机检查:"这真的是 AActor* 吗?"
if (!MyActor->IsA(AActor::StaticClass())) {
Error();
}
安全有一个小成本!
反射系统使用
蓝图对所有事情都使用反射:
1
2
3
4
5
6
7
8
// C++(直接访问)
float Health = Actor->Health; // 直接内存读取
// 时间: 1 纳秒
// 蓝图(反射)
FProperty* Prop = FindProperty("Health"); // 查找!
float Health = Prop->GetFloatValue(Actor); // 间接读取!
// 时间: 10-50 纳秒
反射灵活但更慢!
真实性能数字
让我们对常见操作进行基准测试:
变量访问:
- C++: 1-2 ns
- 蓝图: 5-10 ns
- 开销: 5-10 倍
函数调用:
- C++: 5-10 ns
- 蓝图: 50-100 ns
- 开销: 10 倍
数学操作:
- C++: 1 ns
- 蓝图: 2-5 ns
- 开销: 2-5 倍
当蓝图足够快时
开销是绝对时间,不是百分比:
1
2
3
4
5
6
// 昂贵的操作(1 毫秒)
RenderComplexMesh();
// 添加蓝图开销(100 纳秒)
// 总计: 1.0001 毫秒
// 差异: 0.01%(察觉不到!)
如果你的函数做实际工作,蓝图开销就消失了!
当蓝图受伤时
紧循环是痛苦的:
1
2
3
4
5
6
7
8
9
10
// 蓝图(坏!)
For i = 0 to 10000:
Result = Result + Array[i]
// 10,000 次函数调用 × 100ns = 损失 1 毫秒!
// C++(好)
for (int i = 0; i < 10000; i++) {
Result += Array[i];
}
// 直接内存访问 = 微秒,而且现代编译器会直接优化为 O(1),因为我们有等差数列求和公式!
热路径很重要: 每帧调用的函数”应该”是 C++!但这真的取决于这些函数中实际做了什么。
优化策略
保留在蓝图中:
- 高级游戏逻辑
- 事件处理器
- UI 更新
- 不频繁的操作
移到 C++:
- 紧循环
- 数学密集型算法
- 每帧计算
- 性能关键路径
本地化(RIP)
虚幻有蓝图本地化:
- 将蓝图转换为 C++
- 编译为本地代码
- 移除所有开销!
它被移除是因为:
- 难以维护
- 二进制膨胀
- 调试困难
热重载比本地化更有价值!
未来:Verse
Epic 的新语言 Verse 旨在解决这个问题:
- 编译时优化
- 零复制函数调用
- 本地性能
- 可视化脚本的好处
蓝图不会消失,但 Verse 将处理性能关键代码!
快速要点
- 蓝图的慢来自复制,而不是解释
- 每个函数调用复制所有参数
- 运行时栈管理增加开销
- 反射灵活但比直接访问慢
- 典型开销:简单操作慢 5-10 倍
- 开销对昂贵的操作不重要
- 紧循环和热路径应该是 C++
- 为高级逻辑保留蓝图
性能权衡
蓝图用原始速度交换:
- 可视化编辑
- 快速迭代
- 热重载
- 对设计师友好
- 反射能力
对于大多数游戏逻辑,这种权衡绝对值得。只有当性能分析显示重要时才优化到 C++!
想要更多细节?
完整的性能分析:
下一篇:创建你自己的自定义蓝图节点!
🍿 BPVM 小食包系列
- ← #17: 字节码中的函数调用
- #18: 为什么蓝图更慢 ← 你在这里
- #19: 自定义蓝图 →
本文由作者按照
CC BY 4.0
进行授权