从我的小型研究中,下面的两个函数是等效的(从性能的角度来看),因为即使非常轻微的优化(-O1
)也会产生相同的汇编代码。
代码1:
#define BIT_N (10)
extern unsigned int isBitSet;
unsigned int Foo() {
unsigned int res1 = 0;
if (isBitSet)
{
res1 |= ( 1u << BIT_N );
}
return res1;
}
代码2:
#define BIT_N (10)
extern unsigned int isBitSet;
unsigned int Foo() {
unsigned int res1 = 0;
res1 |= ( (!!isBitSet) << BIT_N );
return res1;
}
Code 1 和 Code 2 的反汇编是相同的(在使用gcc 6.3进行x86_64 +优化-O2
编译之后):
Foo():
mov edx, DWORD PTR isBitSet[rip]
xor eax, eax
test edx, edx
setne al
sal eax, 10
ret
我个人更喜欢 Code 2 的C版本,它对我来说看起来更干净。但是它和 Code 1 一样安全吗?因为我发现一些潜在的陷阱,例如isBitSet
类型为int
和BIT_N
31,所以代码res1 = ( (!!isBitSet) << BIT_N);
会导致未定义的行为。
问题(S):
还有其他陷阱吗? Code 1 是否比 Code 2 更安全? 如果是,是否有任何已知的方法可以使 Code 2 更安全,而不会产生太多开销?
答案 0 :(得分:2)
因为我看到一些潜在的陷阱,例如isBitSet是int类型和BIT_N 31
你已经经历了另一个陷阱,即你认为isBitSet
的类型很重要。 C中的所有“逻辑”运算符(例如!
)都返回值为{0}的类型int
。
所以表达式(!!isBitSet) << BIT_N
每个设计都是危险的,不应该使用,因为提到的未定义行为。
不应该使用它的另一个原因是程序员可能错误地认为表达式的结果类型是isBitSet
的类型。因此,如果程序员会编写一些东西,例如依赖于无符号环绕,就像这样:
((!!isBitSet) << BIT_N) + UINT16_C(something)
然后这也会导致未定义的行为= 32位系统上的整数溢出,因为+
的左操作数是有符号的,这不是预期的。
问题的根源是在一行上使用多个运算符。这几乎总是不好的做法,可能导致许多错误。我的经验还表明,!!
技巧的存在通常是可疑代码的标志。
您的代码的理想,完全可移植版本将是:
uint32_t Foo (void)
{
uint32_t result;
if(isBitSet)
{
result = 1u << BIT_N;
}
else
{
result = 0;
}
return result;
}
或者,如果你愿意,完全等同于:
uint32_t Foo (void)
{
return isBitSet ? 1u<<BIT_N : 0;
}
答案 1 :(得分:0)
运算符!
返回的类型为int
,因此可以获得UB。 (isBitSet
类型的签名无关紧要。)
从N1570,6.5.7按位移位运算符:
- 醇>
E1 << E2
的结果是E1左移E2位位置;腾出的位充满了 零。如果E1具有无符号类型,则结果的值为E1×2 E2 ,减少模数 比结果类型中可表示的最大值多一个。 如果E1已签名 类型和非负值,E1×2 E2 在结果类型中可表示,那么 结果价值;否则,行为未定义。
然而,修复是直截了当的;在转移之前转为无符号类型:
res1 |= (unsigned)(!!isBitSet) << BIT_N;
2的完成机器不应该有任何开销,所以演员基本上是免费的。
答案 2 :(得分:0)
type of !!isBitSet
is int
(值为0
或1
)。随后的left-shift results in undefined behavior if the resulting value cannot be represented as an int
。例如,假设32位int
和2的补码,如果!!isBitSet == 1
和BIT_N >= 31
(因为那时1位移入或超出符号位),就会发生这种情况。 。所以,你提到的潜在陷阱确实是一个。
除此之外,代码定义良好,并且两个函数具有相同的行为。所以,只要你避免上面提到的未定义行为的条件,第二个函数就等同于第一个函数。
也就是说,第一个应该是首选,因为它更明确地表达了它的意图(因此更容易阅读),并且没有机会出现未定义的行为。