BPVM 小食包 #15 - 优化:让你的蓝图更快 | ebp BPVM 小食包 #15 - 优化:让你的蓝图更快 | ebp BPVM 小食包 #15 - 优化:让你的蓝图更快 | ebp
文章

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 小食包系列

本文由作者按照 CC BY 4.0 进行授权