BPVM 小食包 #15 - 优化:让你的蓝图更快
编译器不只是翻译你的节点 - 它还优化它们!了解让编译后的蓝图运行更快的巧妙技巧。
BPVM 小食包 #15 - 优化:让你的蓝图更快
本文内容基于Unreal Engine 5.6.0
BPVM 小食包 - 蓝图知识快速投喂!是蓝图到字节码系列的一部分。
优化阶段
在调度之后但在字节码生成之前,编译器优化你的语句:
1
2
3
4
5
6
7
8
void PostcompileFunction(Context) {
Context.ResolveStatements(); // 优化发生在这里!
// 在 ResolveStatements 内部:
FinalSortLinearExecList(); // 重新排序以提高效率
ResolveGoToFixups(); // 修复跳转目标
MergeAdjacentStates(); // 合并操作
}
你的代码在你不做任何事的情况下变得更快!
优化 #1: 合并相邻状态
移除冗余的推送/弹出操作:
1
2
3
4
5
6
7
// 优化前
PushState(Label_A)
PopState()
PushState(Label_B)
// 优化后
PushState(Label_B) // 前两个移除!
为什么重要: 流栈操作昂贵。更少 = 更快!
优化 #2: 移除冗余跳转
消除无用的跳转:
1
2
3
4
5
6
7
// 之前
Goto Label_A
Label_A: // 跳转目标就在这里!
DoSomething()
// 之后
DoSomething() // 跳转移除!
为什么重要: 每个跳转都有开销。没有跳转 = 立即执行!
优化 #3: 死代码消除
移除永远不会运行的代码:
1
2
3
4
5
6
7
// 之前
Return
CallFunction() // 永远不会到达!
SetVariable() // 永远不会到达!
// 之后
Return // 之后的所有内容移除!
为什么重要: 为什么生成永远不会执行的字节码?
优化 #4: 常量折叠
预计算常量表达式:
1
2
3
4
5
// 之前
Result = 5 + 10 + 15
// 之后
Result = 30 // 在编译时计算!
为什么重要: 为什么在你已经知道的数学上浪费 CPU 周期?
优化 #5: 跳转链折叠
简化跳转链:
1
2
3
4
5
6
7
8
9
// 之前
JumpIfFalse Label_A
Label_A: Jump Label_B
Label_B: Jump Label_C
Label_C: DoSomething()
// 之后
JumpIfFalse Label_C // 直接跳转!
DoSomething()
为什么重要: 每个跳转都需要时间。一个跳转而不是三个!
优化 #6: 流栈 vs 直接返回
选择更快的路径:
1
2
3
4
5
6
7
// 复杂流程(需要流栈)
BeginPlay → Branch
True → DoA → EndOfThread
False → DoB → EndOfThread
// 简单流程(直接返回)
BeginPlay → DoSimpleStuff → Return // 无流栈!
为什么重要: 流栈管理慢。直接返回快!
MergeAdjacentStates 算法
这是最有影响力的优化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void MergeAdjacentStates() {
for (int i = 0; i < Statements.Num(); i++) {
Statement* Current = Statements[i];
Statement* Next = Statements[i+1];
// 模式: 推送然后弹出
if (Current->Type == KCST_PushState &&
Next->Type == KCST_EndOfThread) {
// 移除两者!
Statements.RemoveAt(i, 2);
i--;
}
// 模式: 跳转到下一个语句
if (Current->Type == KCST_UnconditionalGoto &&
Current->TargetLabel == Next->Label) {
// 移除跳转!
Statements.RemoveAt(i);
i--;
}
}
}
真实世界影响
优化前:
1
2
3
4
15 个语句
8 个跳转
4 个流栈操作
字节码大小: 512 字节
优化后:
1
2
3
4
10 个语句 (减少 33%!)
3 个跳转 (减少 62%!)
1 个流栈操作 (减少 75%!)
字节码大小: 320 字节 (减少 37%!)
结果: 更快的执行和更小的内存占用!
纯节点优化
纯节点获得特殊处理:
1
2
3
4
5
6
// 如果输出从未使用
GetRandomFloat() // 移除!
// 如果输出只使用一次
GetRandomFloat() → Add → Print
// 全部内联在一起!
为什么重要: 不要计算没人需要的值!
分支预测提示
编译器尝试优化分支:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 最常见的模式
if (IsValid()) { // 可能为真
DoStuff();
} else { // 很少发生
HandleError();
}
// 编译器安排:
CheckIsValid()
JumpIfFalse Error_Label // 不太可能跳转
DoStuff()
Jump End_Label
Error_Label: HandleError() // 冷代码
End_Label:
为什么重要: CPU 分支预测工作得更好!
优化限制
有些事情无法优化:
1
2
3
4
5
6
7
8
// 无法优化外部调用
CallBlueprintFunction() // 未知行为
// 无法优化动态转换
Cast<AMyActor>(GetActor()) // 运行时检查
// 无法优化面向用户的调试站点
BreakPoint() // 必须为调试保留!
性能影响
典型的优化收益:
- 10-20% 更快的执行
- 20-30% 更小的字节码
- 更少的虚拟机开销操作
- 更好的缓存局部性
不是很戏剧性,但完全免费!
快速要点
- 编译器自动优化你的蓝图
- MergeAdjacentStates 移除冗余流操作
- 跳转消除使控制流更快
- 死代码移除缩小字节码大小
- 常量折叠预计算已知值
- 典型收益:10-20% 更快,20-30% 更小
- 你免费获得这些好处!
沉默的优化器
下次你编译蓝图时,请记住,编译器不只是翻译你的节点 - 它在积极地使它们更快。就像有一个专家程序员审查并优化你写的每个函数,自动地!
想要更多细节?
完整的优化分解:
下一篇:学习阅读实际的字节码!
🍿 BPVM 小食包系列
- ← #14: 后端魔法
- #15: 优化解释 ← 你在这里
- #16: 阅读字节码 →
本文由作者按照
CC BY 4.0
进行授权