简化(A & B) && !(A & C)

12

A、B和C是某种无符号整型变量。从概念上讲,A是一个测试向量,B是“必需”位的掩码(至少一个对应的位在A中必须设置),而C是“禁止”位的掩码(A中不能设置任何对应的位)。由于我们同时使用位运算和逻辑运算符,因此通常看来合理的解决方案为

A & B & ~C

是不正确的。相反,标题表达式等同于伪代码

((a0 & b0) | ... | (an & bn)) & (~(a0 & c0) & ... & ~(an & cn))

a0等代表单独的位(n是最高位的索引)。我不知道如何有效地重新排列并提取相应的代码,但还是有没有巧妙的方法,也许用^来简化标题中的表达式?

编辑:受@huseyintugrulbuyukisik的问题激发,我注意到我们可以假设(B&C)==0,但我不知道这是否有帮助。

编辑2:结果:这取决于分支预测的好坏!

#include <chrono>
#include <cmath>
#include <iostream>
#include <vector>

using UINT = unsigned int;
int main(void)
{
    const auto one = UINT(1);
    const UINT B = (one << 9); // Version 1
//  const UINT B = (one << 31) - 1;  // Version 2
    const UINT C = (one << 5) | (one << 15) | (one << 25);

    const size_t N = 1024 * 1024;
    std::vector<UINT> vecA(N);
    for (size_t i = 0; i < N; ++i)
        vecA[i] = (UINT)rand();

    int ct = 0; //To avoid compiler optimizations
    auto tstart = std::chrono::steady_clock::now();
    for (size_t i = 0; i < N; ++i)
    {
        const UINT A = vecA[i];
        if ((A & B) && !(A & C))
            ++ct;
    }
    auto tend = std::chrono::steady_clock::now();
    auto tdur = std::chrono::duration_cast<std::chrono::milliseconds>(tend - tstart).count();
    std::cout << ct << ", " << tdur << "ms" << std::endl;

    ct = 0;
    tstart = std::chrono::steady_clock::now();
    for (size_t i = 0; i < N; ++i)
    {
        const UINT A = vecA[i];
        if (!((!(A & B)) | (A & C)))
            ++ct;
    }
    tend = std::chrono::steady_clock::now();
    tdur = std::chrono::duration_cast<std::chrono::milliseconds>(tend - tstart).count();
    std::cout << ct << ", " << tdur << "ms" << std::endl;

    return 0;
}

版本 1:

$ ./ops_test 
    65578, 8ms
    65578, 3ms

版本 2:

$ ./ops_test
    130967, 4ms
    130967, 4ms

这些是代表性的数值(实际上我运行了多次每个测试)。g++ 4.8.4,默认优化。我只在B中设置了4位,就得到类似版本2的结果。然而,我的使用情况仍更接近版本1,所以我认为@DougCurrie的答案是一个改进。


5
看起来它已经相当简单了。 - Baum mit Augen
1
B和C的二进制位上有重叠吗? - huseyin tugrul buyukisik
4
@MattPhilips说,位运算本身是CPU能够执行的最便宜的操作,所以不用太担心它们。更耗费资源的可能是&&,因为它通常涉及到分支。但是,像往常一样,在进行优化之前要先进行测量,如果你的“每个周期都很重要”的部分实际上受到 - 比如 - 从RAM中获取未在缓存中的数据的限制,那么担心这种问题可能完全没有意义。 - Matteo Italia
1
分支现在非常便宜,分支预测器也非常出色。 - Baum mit Augen
2
@MattPhillips 由于C++包含C,这个问题肯定经常出现。 C++和C是两种不同的语言,有各自独立的标准,它们之间并不存在子集关系。 是否有任何书面指南说明应该只选择一个标签? 标签应该最好描述问题的主题。本问题的内容不支持包含C标签。 - 2501
显示剩余15条评论
2个回答

8

!(A & B) 必须为零

A & C 必须为零

因此

(!(A & B)) | (A & C) 必须为零

这可以节省与 && 相关的分支,一些编译器也可以将 ! 优化成无需分支的形式。


“~A&B必须为零”不正确,因为只需要在A中设置B的1位即可通过,而不是所有位都需要设置。 - samgak
我使用 ! 进行了修复。 - Doug Currie
1
那么测试就是 (!((!(A & B)) | (A & C))),我猜?我需要对此进行分析,但这种方法值得肯定,很可能是最好的选择。 - Matt Phillips
2
在我看来,这里唯一真正的区别是使用位运算逻辑而不是短路逻辑。它同样可以写成!!(A & B) & !(B & C),在这种情况下,有人可能会认为后者更好。 ;) - Dolda2000
我不会说它更简单,但我会说它更好! - ScegfOd

4
可以承认,我找不到它的数学证明,但我认为你的表达式不能进一步简化,至少不能简化为纯位逻辑。
原因是两个测试(A&B和!(A&C))是两种不同类型的测试:第一个测试是否有任何位是这样或那样(在这种情况下为1),而另一个测试是否所有位都是这样或那样(在这种情况下为0)。
在所有情况下,要将最终位数组转换为单个布尔值,您需要某些操作将所有位合并为一个位(例如!或隐式的if子句中的!= 0)。由于上述原因,您需要两个不同的此类合并运算符。我的理解是,通过“简化”表达式,您意味着将其转换为全位操作,这意味着仅使用if子句中隐含的一个合并运算符-如果我正确,这是不够的。
最后,我可能会补充说,即使可以按某种标准简化表达式,我也不确定是否应该这样做。毕竟,它的当前形式很好地表达了实际意图:“这些,但不是那些”。

2
这是误导性的,因为计算机也可以进行算术运算。假设,举个简单的例子,我们知道符号位不在任何一个位掩码中。那么,(A&B)-1 是负数,当且仅当 A&B 为0时。同样地,-(A&C) 是负数,当且仅当 A&C 不为0时。换句话说,这两个表达式必须都为正才能使原始测试为真,因此测试 (((A&B)-1)|-(A&C)) >= 0 就足够了(或者用位运算检查符号位替换 >=0)。对于可移植的C语言,您需要使用有符号/无符号转换来进行更多的操作,这些操作无法在此评论中完成,但肯定是可行的。 - rici
@rici:确实,如果这是目标,那确实简化了测试为所有ALU操作 - Dolda2000
通常位运算的目标是(1)找到绝对最快的解决方案和(2)将解决方案混淆到难以理解的程度。我认为上述至少满足(2)。需要进行基准测试以证明(1)(或不是)。 - rici
@rici: "简化"的另一个合理解释可能是删除表达式中“冗余”的A提及,不过我仍在思考。当然,我很乐意为优化表达式的难读性额外奖励积分。 - Dolda2000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接