关键发现:
这个对比实验揭示了一个重要原则:无分支优化并非银弹。当分支本身高度可预测时,其开销可能远小于无分支化引入的额外计算开销。例如,项目中常见的错误检查分支(如 06
|
| 优化方法 | 函数 | 规律数据 | 随机数据 |
|---|---|---|---|
| 原始分支 | branch_func |
2,864 | 17,876 |
| 数组查表 | nobranch_func_1 |
2,549 | 17,561 |
| cmove尝试 | nobranch_func_2 |
2,863 | 17,878 |
| 算术掩码 | nobranch_func_3 |
2,549 | 17,560 |
分支预测失败数 (branch-misses,单位:百万次):
| 优化方法 | 函数 | 规律数据 | 随机数据 |
|---|---|---|---|
| 原始分支 | branch_func |
106 | 265 |
| 数组查表 | nobranch_func_1 |
106 | 108 |
| cmove尝试 | nobranch_func_2 |
106 | 266 |
| 算术掩码 | nobranch_func_3 |
106 | 107 |
cmove 尝试彻底失败:nobranch_func_2 的分支数和分支预测失败数与原始版本 branch_func 几乎完全相同。这确凿地证明,编译器并未将三元运算符编译为无分支的 cmov 指令,而是仍然生成了条件分支指令。这也解释了其耗时为何与原始版本无异。nobranch_func_1 和 nobranch_func_3 的分支数,在两种数据下均比原始版本稳定减少了约300M次(正好等于循环次数 N)。这直接证明了这两种方法有效移除了循环内部的关键分支语句。nobranch_func_1 和 nobranch_func_3 的分支预测失败数从原始版本的 ~265M 大幅降低至 ~107M。这 ~158M 的减少量,正是被消除的300M关键分支在随机数据下预期的预测失败次数(50%失败率),与理论相符。perf 的数据清晰地量化了每种优化手段的实际效果,是性能分析与优化中不可或缺的工具。
以下是完整的测试代码,包含了基准函数、三种优化实现以及数据生成和测试逻辑:
#include<iostream>
#include<chrono>
#include<string>
#include<vector>
const int N = 300*1024*1024;
#define NOTOPTIMIZE(a) asm volatile("" : "+m,r"(a) : : "memory")
// 简单的计时工具类,记录构造到析构的耗时并打印
class TimeState {
public:
TimeState(const std::string& name): name_(name) {
start_ = std::chrono::high_resolution_clock::now();
}
~TimeState() {
auto diff = std::chrono::high_resolution_clock::now() - start_;
std::cout << name_ << " cost time:\t"
<< std::chrono::duration_cast<std::chrono::milliseconds>(diff).count()
<< " ms, \t per cost:\t"
<< std::chrono::duration_cast<std::chrono::nanoseconds>(diff*10/N).count()
<< " *0.1ns" << std::endl;
}
private:
std::string name_;
std::chrono::system_clock::time_point start_;
};
void branch_func(const std::vector<int>& p1,
const std::vector<int>& p2,
const std::vector<int8_t>& b1,
int& a1, int& a2) {
int data_size = p1.size();
TimeState ts("branch");
for (int i = 0; i < data_size; i++) {
if (b1[i]) {
a1 += p1[i] - p2[i];
} else {
a2 += p1[i] + p2[i];
}
}
NOTOPTIMIZE(a1);
NOTOPTIMIZE(a2);
}
void nobranch_func_1(const std::vector<int>& p1,
const std::vector<int>& p2,
const std::vector<int8_t>& b1,
int& a1, int& a2) {
int data_size = p1.size();
int* a[2] = {&a2, &a1};
TimeState ts("nobranch_1");
for (int i = 0; i < data_size; i++) {
int s[2] = {p1[i] + p2[i], p1[i] - p2[i]};
*a[b1[i]] += s[b1[i]];
}
NOTOPTIMIZE(a1);
NOTOPTIMIZE(a2);
}
void nobranch_func_2(const std::vector<int>& p1,
const std::vector<int>& p2,
const std::vector<int8_t>& b1,
int& a1, int& a2) {
int data_size = p1.size();
int d1 = 0;
int d2 = 0;
TimeState ts("nobranch_2");
for (int i = 0; i < data_size; i++) {
const int diff = p1[i] - p2[i]+d1;
const int sum = p1[i] + p2[i]+d2;
d1 = b1[i] ? diff: d1;
d2 = b1[i] ? d2 : sum;
}
a1 += d1;
a2 += d2;
NOTOPTIMIZE(a1);
NOTOPTIMIZE(a2);
}
void nobranch_func_3(const std::vector<int>& p1,
const std::vector<int>& p2,
const std::vector<int8_t>& b1,
int& a1, int& a2) {
int data_size = p1.size();
TimeState ts("nobranch_3");
for (int i = 0; i < data_size; i++) {
a1 += b1[i] * (p1[i] - p2[i]);
a2 += (!b1[i]) * (p1[i] + p2[i]);
}
NOTOPTIMIZE(a1);
NOTOPTIMIZE(a2);
}
void regular_input_test(){
std::vector<int> p1,p2;
std::vector<int8_t> b1;
int a1 = 0, a2 = 0;
p1.reserve(N);
p2.reserve(N);
b1.reserve(N);
std::cout << "regular input data" << std::endl;
for (int i = 0; i < N; ++i) {
p1.emplace_back(i);
p2.emplace_back(i+1);
b1.emplace_back(i&0x1);
}
branch_func(p1, p2, b1, a1, a2);
nobranch_func_1(p1, p2, b1, a1, a2);
nobranch_func_2(p1, p2, b1, a1, a2);
nobranch_func_3(p1, p2, b1, a1, a2);
}
void random_input_test(){
std::vector<int> p1,p2;
std::vector<int8_t> b1;
int a1 = 0, a2 = 0;
p1.reserve(N);
p2.reserve(N);
b1.reserve(N);
std::cout << "random input data" << std::endl;
for (int i = 0; i < N; ++i) {
p1.emplace_back(rand());
p2.emplace_back(rand());
b1.emplace_back(rand()&0x1);
}
branch_func(p1, p2, b1, a1, a2);
nobranch_func_1(p1, p2, b1, a1, a2);
nobranch_func_2(p1, p2, b1, a1, a2);
nobranch_func_3(p1, p2, b1, a1, a2);
}
int main(){
regular_input_test();
std::cout << "-----------" << std::endl;
random_input_test();
return 0;
}
编译并运行:
g++ nobranch.cpp -g -O2 && ./a.out
运行结果:
regular input data
branch cost time: 189 ms, per cost: 6 *0.1ns
nobranch_1 cost time: 336 ms, per cost: 10 *0.1ns
nobranch_2 cost time: 185 ms, per cost: 5 *0.1ns
nobranch_3 cost time: 326 ms, per cost: 10 *0.1ns
-----------
random input data
branch cost time: 1052 ms, per cost: 33 *0.1ns
nobranch_1 cost time: 358 ms, per cost: 11 *0.1ns
nobranch_2 cost time: 1013 ms, per cost: 32 *0.1ns
nobranch_3 cost time: 322 ms, per cost: 10 *0.1ns
本文通过一个具体的 C++ 代码示例,深入实践并对比了三种无分支优化方法:
nobranch_func_1):成功消除分支,在随机数据上性能提升约3倍。代价是引入了额外的数组内存访问和计算。perf 数据确认了分支的消除。cmov) 尝试 (nobranch_func_2):优化未生效。perf 证实编译器未生成 cmov 指令,其分支行为与原始版本一致。这表明依赖编译器进行此类优化需要实际验证,不能仅凭编码直觉。nobranch_func_3):成功消除分支,在随机数据上性能提升约 3.3倍(最佳)。代价是引入了额外的乘法和逻辑非运算。代码简洁,是实践中非常有效的技巧。核心洞见与实践指导:
perf 等工具对目标分支的预测失败率进行量化分析,避免盲目优化。掌握高效的编程技巧,并结合扎实的性能分析,才能让我们在性能优化的道路上走得更稳、更远。希望这篇实践解析能为你带来启发,也欢迎在 云栈社区 交流探讨更多性能优化话题。