IF(A|B)总是比IF(A||B)快吗?
我正在阅读Fedor Pikus的this book,他有一些非常非常有趣的例子,对我来说是一个惊喜。
尤其是这个基准测试让我印象深刻,唯一的区别是,在其中一个基准测试中,我们在IF中使用||,在另一个基准测试中,我们使用|。
void BM_misspredict(benchmark::State& state)
{
std::srand(1);
const unsigned int N = 10000;;
std::vector<unsigned long> v1(N), v2(N);
std::vector<int> c1(N), c2(N);
for (int i = 0; i < N; ++i)
{
v1[i] = rand();
v2[i] = rand();
c1[i] = rand() & 0x1;
c2[i] = !c1[i];
}
unsigned long* p1 = v1.data();
unsigned long* p2 = v2.data();
int* b1 = c1.data();
int* b2 = c2.data();
for (auto _ : state)
{
unsigned long a1 = 0, a2 = 0;
for (size_t i = 0; i < N; ++i)
{
if (b1[i] || b2[i]) // Only difference
{
a1 += p1[i];
}
else
{
a2 *= p2[i];
}
}
benchmark::DoNotOptimize(a1);
benchmark::DoNotOptimize(a2);
benchmark::ClobberMemory();
}
state.SetItemsProcessed(state.iterations());
}
void BM_predict(benchmark::State& state)
{
std::srand(1);
const unsigned int N = 10000;;
std::vector<unsigned long> v1(N), v2(N);
std::vector<int> c1(N), c2(N);
for (int i = 0; i < N; ++i)
{
v1[i] = rand();
v2[i] = rand();
c1[i] = rand() & 0x1;
c2[i] = !c1[i];
}
unsigned long* p1 = v1.data();
unsigned long* p2 = v2.data();
int* b1 = c1.data();
int* b2 = c2.data();
for (auto _ : state)
{
unsigned long a1 = 0, a2 = 0;
for (size_t i = 0; i < N; ++i)
{
if (b1[i] | b2[i]) // Only difference
{
a1 += p1[i];
}
else
{
a2 *= p2[i];
}
}
benchmark::DoNotOptimize(a1);
benchmark::DoNotOptimize(a2);
benchmark::ClobberMemory();
}
state.SetItemsProcessed(state.iterations());
}
我不会详述本书中解释的后者为什么更快的所有细节,但其想法是,硬件分支预测器在较慢的版本和|(按位或)版本中有两次误判机会。请参阅下面的基准测试结果。
所以问题是,为什么我们不总是在分支中使用|而不是|?
解决方案
if(A | B)
总是比if(A || B)
快吗?
否,if(A | B)
并不总是比if(A || B)
快。
A
为真,并且B
表达式是非常昂贵的操作。不做手术可以节省这笔费用。
所以问题是,为什么我们不总是在分支中使用|而不是|?
除了逻辑或更有效的情况外,效率并不是唯一需要考虑的问题。通常存在具有前置条件的操作,并且存在左手操作的结果表示右手操作是否满足前置条件的情况。在这种情况下,我们必须使用逻辑运算符。
if (b1[i]) // maybe this exists somewhere in the program
b2 = nullptr;
if(b1[i] || b2[i]) // OK
if(b1[i] | b2[i]) // NOT OK; indirection through null pointer
当优化器不能按位替换逻辑时,这种可能性通常就是问题所在。在if(b1[i] || b2[i])
的例子中,优化器只有在能够证明b2
至少在b1[i] != 0
时有效的情况下才能进行这样的替换。这种情况在您的示例中可能不存在,但这并不意味着优化器证明它不存在就一定很容易,有时甚至是可能的。
此外,可能依赖于操作的顺序,例如,如果一个操作数修改了另一个操作读取的值:
if(a || (a = b)) // OK
if(a | (a = b)) // NOT OK; undefined behaviour
还有一些类型可以转换为bool,因此是||
的有效操作数,但不是|
的有效运算符:
if(ptr1 || ptr2) // OK
if(ptr1 | ptr2) // NOT OK; no bitwise or for pointers
TL;DR如果我们总是可以使用按位或而不是逻辑运算符,那么就不需要逻辑运算符了,它们很可能也不会出现在语言中。但这样的替换并不总是可能的,这就是我们使用逻辑运算符的原因,也是优化器有时不能使用较快选项的原因。
相关文章