在计算中使用布尔值以避免分支
Using bools in calculations to avoid branches
这是我提出的一点微观优化好奇心:
struct Timer {
bool running{false};
int ticks{0};
void step_versionOne(int mStepSize) {
if(running) ticks += mStepSize;
}
void step_versionTwo(int mStepSize) {
ticks += mStepSize * static_cast<int>(running);
}
};
似乎这两种方法实际上做同样的事情。第二个版本是否避免了分支(因此比第一个版本更快(,或者是否有任何编译器能够使用 -O3
进行这种优化?
是的,你的技巧可以避免分支,它使它更快......有时。
我写了基准测试,比较了各种情况下的这些解决方案,以及我自己的解决方案:
ticks += mStepSize & -static_cast<int>(running)
我的结果如下:
Off:
branch: 399949150
mul: 399940271
andneg: 277546678
On:
branch: 204035423
mul: 399937142
andneg: 277581853
Pattern:
branch: 327724860
mul: 400010363
andneg: 277551446
Random:
branch: 915235440
mul: 399916440
andneg: 277537411
Off
是计时器关闭的时间。在这种情况下,解决方案需要大约相同的时间。
On
是它们打开的时间。分支解决方案的速度提高了两倍。
Pattern
是当它们处于 100110 模式时。性能相似,但分支速度更快一些。
Random
是分支不可预测的时候。在这种情况下,乘法的速度要快 2 倍以上。
在所有情况下,我的位黑客技巧都是最快的,除了分支获胜的On
。
请注意,此基准测试不一定代表所有编译器版本处理器等。即使是基准测试的微小变化也会使结果颠倒过来(例如,如果编译器可以内联知道mStepSize
是1
,那么乘法实际上可能是最快的(。
基准代码:
#include<array>
#include<iostream>
#include<chrono>
struct Timer {
bool running{false};
int ticks{0};
void branch(int mStepSize) {
if(running) ticks += mStepSize;
}
void mul(int mStepSize) {
ticks += mStepSize * static_cast<int>(running);
}
void andneg(int mStepSize) {
ticks += mStepSize & -static_cast<int>(running);
}
};
void run(std::array<Timer, 256>& timers, int step) {
auto start = std::chrono::steady_clock::now();
for(int i = 0; i < 1000000; i++)
for(auto& t : timers)
t.branch(step);
auto end = std::chrono::steady_clock::now();
std::cout << "branch: " << (end - start).count() << std::endl;
start = std::chrono::steady_clock::now();
for(int i = 0; i < 1000000; i++)
for(auto& t : timers)
t.mul(step);
end = std::chrono::steady_clock::now();
std::cout << "mul: " << (end - start).count() << std::endl;
start = std::chrono::steady_clock::now();
for(int i = 0; i < 1000000; i++)
for(auto& t : timers)
t.andneg(step);
end = std::chrono::steady_clock::now();
std::cout << "andneg: " << (end - start).count() << std::endl;
}
int main() {
std::array<Timer, 256> timers;
int step = rand() % 256;
run(timers, step); // warm up
std::cout << "Off:n";
run(timers, step);
for(auto& t : timers)
t.running = true;
std::cout << "On:n";
run(timers, step);
std::array<bool, 6> pattern = {1, 0, 0, 1, 1, 0};
for(int i = 0; i < 256; i++)
timers[i].running = pattern[i % 6];
std::cout << "Pattern:n";
run(timers, step);
for(auto& t : timers)
t.running = rand()&1;
std::cout << "Random:n";
run(timers, step);
for(auto& t : timers)
std::cout << t.ticks << ' ';
return 0;
}
Does the second version avoid a branch
如果你编译代码以获得汇编器输出,g++ -o test.s test.cpp -S
,你会发现第二个函数中确实避免了分支。
and consequently, is faster than the first version
我运行了您的每个函数2147483647
或INT_MAX
次数,在每次迭代中,我使用以下代码将布尔值随机分配给Timer
结构running
成员:
int main() {
const int max = std::numeric_limits<int>::max();
timestamp_t start, end, one, two;
Timer t_one, t_two;
double percent;
srand(time(NULL));
start = get_timestamp();
for(int i = 0; i < max; ++i) {
t_one.running = rand() % 2;
t_one.step_versionOne(1);
}
end = get_timestamp();
one = end - start;
std::cout << "step_versionOne = " << one << std::endl;
start = get_timestamp();
for(int i = 0; i < max; ++i) {
t_two.running = rand() % 2;
t_two.step_versionTwo(1);
}
end = get_timestamp();
two = end - start;
percent = (one - two) / static_cast<double>(one) * 100.0;
std::cout << "step_versionTwo = " << two << std::endl;
std::cout << "step_one - step_two = " << one - two << std::endl;
std::cout << "one fast than two by = " << percent << std::endl;
}
这些是我得到的结果:
step_versionOne = 39738380
step_versionTwo = 26047337
step_one - step_two = 13691043
one fast than two by = 34.4529%
所以是的,第二个功能显然更快,大约 35%。 请注意,对于较少的迭代次数,定时性能的百分比增加在 30% 到 55% 之间变化,而运行时间越长,它似乎稳定在 35% 左右。 这可能是由于在模拟运行时零星执行系统任务, 它变得不那么零星,即运行模拟的时间越长,一致性(虽然这只是我的假设,但我不知道这是否真的是真的(
总而言之,很好的问题,我今天学到了一些东西!
<小时 />更多:
<小时 />当然,通过随机生成running
,我们本质上是在第一个函数中渲染分支预测无用,所以上面的结果并不太令人惊讶。 但是,如果我们决定在循环迭代期间不更改running
,而是将其保留为默认值,在这种情况下false
,分支预测将在第一个函数中发挥其魔力, 并且实际上会快近 20%,如这些结果表明的那样:
step_versionOne = 6273942
step_versionTwo = 7809508
step_two - step_one = 1535566
two fast than one by = 19.6628
由于running
在整个执行过程中都是恒定的,因此请注意,模拟时间比随机更改running
时短得多 - 可能是编译器优化的结果。
为什么在这种情况下第二个函数更慢? 好吧,分支预测将很快意识到第一个函数中的条件永远不会满足,因此首先会停止检查(就好像if(running) ticks += mStepSize;
甚至不存在一样(。 另一方面,第二个函数仍然必须在每次迭代中ticks += mStepSize * static_cast<int>(running);
执行此指令, 从而使第一个功能更有效率。
但是,如果我们running
设置为 true
呢? 好吧,分支预测将再次启动,但是,这一次,第一个函数必须在每次迭代中评估ticks += mStepSize;
; 以下是running{true}
时的结果:
step_versionOne = 7522095
step_versionTwo = 7891948
step_two - step_one = 369853
two fast than one by = 4.68646
请注意,无论running
是不断true
还是false
,step_versionTwo
都需要一致的时间。 但它仍然比step_versionTwo
花费更长的时间,尽管幅度很小。 好吧,这可能是因为我懒得运行很多次来确定它是始终更快还是一次性侥幸(每次运行它时结果略有不同, 因为操作系统必须在后台运行,并且并不总是会做同样的事情(。但如果它一直更快,那可能是因为函数二(ticks += mStepSize * static_cast<int>(running);
(的算术运算比函数一(ticks += mStepSize;
(多。
最后,让我们使用优化 - g++ -o test test.cpp -std=c++11 -O1
进行编译,让我们将running
恢复到false
,然后检查结果:
step_versionOne = 704973
step_versionTwo = 695052
编译器将执行其优化传递,并意识到running
始终false
,因此,出于所有意图和目的,将删除step_versionOne
的主体,因此当您从main
中的循环中调用它时,它只会调用函数并返回。
另一方面,在优化第二个函数时,它将意识到ticks += mStepSize * static_cast<int>(running);
将始终生成相同的结果,即 0
,所以它也不会打扰执行。
总而言之,如果我是正确的(如果不是,请纠正我,我对此很陌生(,从 main
循环调用这两个函数时,您将获得的只是它们的开销。
附言这是使用优化编译时第一种情况的结果(running
在每次迭代中随机生成(。
step_versionOne = 18868782
step_versionTwo = 18812315
step_two - step_one = 56467
one fast than two by = 0.299261
- 在没有定义返回类型的函数中返回布尔值,并将结果保存在无错误的char编译中-为什么
- 变量定义到C++布尔值转换
- 如何确保在使用基于布尔值的两个方法之一调用方法时避免分支预测错误
- 重载更少,则运算符返回相反的布尔值
- 将此布尔值传递给此函数的最有效方法是什么?
- 如何设置 c++ 类的布尔值?
- 使用 MAKEWORD / MAKEWPARAM 使用布尔值而不是布尔值
- 将 10 个线程与原子布尔值同步
- 创建类似于布尔值的变量类型
- 布尔值向量的基于范围 for 循环
- 零点和布尔值之间的比较
- 简化对两个布尔值的 4 个 if/else 检查
- 无法创建带有布尔值和矢量的地图
- 对于完成布尔值设置为 true 后未停止的循环
- fstream / ifstream / ofstream 对象如何转换为布尔值
- C++:将值赋值给原始数据类型(例如布尔值)是原子操作吗?
- 为什么布尔值不能比作最后一点?
- 如何使用返回布尔值的函数?
- 将uintptr_t转换为布尔值会使 SSO 基准速度减慢数倍
- 在计算中使用布尔值以避免分支