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如果我们总是可以使用按位或而不是逻辑运算符,那么就不需要逻辑运算符了,它们很可能也不会出现在语言中。但这样的替换并不总是可能的,这就是我们使用逻辑运算符的原因,也是优化器有时不能使用较快选项的原因。

相关文章