优化 "i = b ? (i | mask) : (i & ~mask)"

4
我希望能够设置或清除一个 uintX_t t 的(多个)位。 i 是运行时变量(uintX_t)。b 是运行时变量(uintX_t),其被限制为 01mask 是编译时常量。
是否有比以下方式更好的方法:
i = b ? (i | mask) : (i & ~mask)

我希望尽可能避免分支。如果有影响,目标平台是ARM。


i = b 还是 i == b - Hong Ooi
@HongOoi i = (b ? (i | mask) : (i & ~mask)) @HongOoi i =(b?(i | mask):(i&〜mask)) - fadedbee
4个回答

6

利用-1u是所有位都设置的值的事实:

i = (i & ~mask) | (mask & -b);

或者

i ^= (i ^ -b) & mask;

第二种方法可以减少操作次数和代码大小。在超标量架构上,第一种方法可能仍然更快,因为某些操作可以并行执行。

1
“-b”和“-1u”并不是同一回事。如果“b”是任何有符号性质的小整数类型,你最终会得到一个负数。而“-1u”则总是一个正数。在这种特定情况下,这不会成为问题,但在像“(mask & -b) << n”这样的代码中,这将是灾难性的。 - Lundin
1
@Lundin 对的,如果“b”比“int”窄,并且平台不使用二进制补码,则我的答案中的代码实际上存在问题。在这种情况下,需要进行额外的转换“-(unsigned)b”。 - nwellnhof

5

这里的想法是用乘法来替换分支语句,在这种情况下,我们可以根据b的值将每个侧面都设置为零:

i = (i | (mask * b)) & (~mask | (mask * b));

我在Java中进行了测试,如果您想要,我可以发布Java测试代码,以证明它确实有效。 - Guilherme Campos Hazan
现在的问题是:从性能损失的角度来看,分支和乘法哪个更糟? - Jabberwocky
在我的测试中,我使用了 i = 101(二进制),掩码 = 110(二进制) - Guilherme Campos Hazan
你可以很容易地发现,将两个代码添加并分别运行100万次,并测量时间。 - Guilherme Campos Hazan
1
如果你的乘法运算速度较慢(通常值得这样重写),你可以将乘法运算轻松地重写为否定和按位与。 - harold

5
另一个选择:始终将位设置为0(左侧),并可选地将位设置为1(右侧)。
i = (i & ~mask) | (mask * b);

这也是从Guilherme的答案中得出的结论,使用该答案或分配运算符。 - harold
@harold 你的评论有没有漏掉什么?结尾看起来很奇怪。 - user694733
不,我只是没有清楚地格式化它,我的意思是OR可以分配给AND。 - harold

1
最易读的方法是分几步来完成 - 这样做不会影响性能,但会提高可读性。
像位运算符一样,您必须小心隐式类型提升。例如,粗心使用 ~ 倾向于创建隐式提升错误。(三目运算符通过平衡第二个和第三个操作数来静默地提升结果。)
易读、便携、安全的代码:
uintx_t i = ... ;
uintx_t b = ... ;  // 1 or 0

i &= (uintx_t)~mask;   // always clear the bit
i |= mask * b;         // if b is 1, set the bit, otherwise OR with 0

谢谢,我喜欢这种易读的方法。如果在AVR上i是PORTA,这会导致GPIO故障吗?还是这两个语句在编译期间合并为一个语句?我期望PORTA被标记为volatile以防止发生这种情况。 - fadedbee
1
@chrisdew 这会导致GPIO在几个时钟周期内变低。编译器不允许优化代码,因为PORTA是一个易失性寄存器。但在某些硬件上,GPIO端口可能需要几个时钟周期来稳定。 - Lundin

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