如果我想将无符号整数四舍五入为最接近的较小或等于偶数的整数,我可以除以2再乘以2吗?


68

例如 :

f(8)=8
f(9)=8

我可以x = x/2*2;吗?编译器是否有可能优化掉此类表达式?


不允许进行优化来更改定义明确的操作的结果。
Barmar

2
强烈建议使用:x >>= 1; x <<= 1;
user3629249

5
@ user3629249为什么强烈建议呢?与问题中已有的代码相比,它不太清楚地表达其意图,它可以编译为相同的机器代码,因此它不会更快,并且如果要四舍五入到与2不同的数字的最接近的倍数,则没有任何模拟(除非该数字也是碰巧是2的幂。
亚瑟塔卡

因为它更快,所以没有乘法和除法。一个更简单的方法是简单地使用:x &= ~(1)' Where the 1`可以是所需长度的1的字符串。例如:x &= ~(0x07);对于三位,等等
user3629249 '17

1
如果两种方法产生的机器指令相同,则必须将优化设置为最大值。
user3629249

Answers:


86

编译器可以进行喜欢的任何优化,只要它不会在程序中引入任何副作用即可。在您的情况下,它不能取消“ 2”,因为表达式的奇数值将有所不同。

x / 2 * 2如果是整数类型,则严格按计算(x / 2) * 2,并x / 2以整数算术执行x

实际上,这是惯用的舍入技术。


4
我喜欢关于这种习惯用法的说明。它也比面具IMO更直观。
StoryTeller-Unslander Monica

29
当然,编译器允许通过用替换它正确地优化此,例如(x>>1)<<1。IIRC,即使技术上关闭了优化,也有编译器会执行此操作。
MSalters

2
@MSalters:是的,这很愚蠢。我已经修改了。
芭丝谢芭

31
@MSalters:或者,在未签名的情况下(x & 0xfffffffe):)
Matthieu M.

3
@Voo:为什么?这是一个无符号整数。根据定义,该行为是相同的。
MSalters

53

由于您指定的整数是无符号的,因此可以使用简单的掩码进行操作:

x & (~1u)

这会将LSB设置为零,从而产生不大于的立即偶数x。也就是说,ifx的类型不超过unsigned int

当然,您可以强制使其1与Wideer具有相同的类型x,如下所示:

x & ~((x & 1u) | 1u)

但是,在这一点上,您确实应该将这种方法视为混淆处理,并接受Bathsheba的回答。


我当然忘记了标准库。如果包含stdint.h(或cstdint,则应在C ++代码中使用)。您可以让实现处理细节:

uintmax_t const lsb = 1;
x & ~lsb;

要么

x & ~UINTMAX_C(1)

3
您不需要x & (~static_cast<decltype(x)>(1))还是我需要咖啡?
芭丝谢芭

2
@Bathsheba-绝对是我需要喝咖啡。嗯...关于使用任何无符号整数类型,我没有做太多。
StoryTeller-Unslander Monica

1
@Bathsheba-我会的,但是那个C标签...我有点想考虑一下,并且通过产生一个在C和C ++都可以使用的解决方案而变得很酷。
StoryTeller-Unslander Monica's

1
@Bathsheba-但是最多UINT_MAX,不?我如果x是的话unsigned long,还是会在同一条船上。
StoryTeller-Unslander Monica

4
x & (~1u)如果类型x大于,恐怕无法正常工作unsigned int。相反,x & ~1对于所有类型,其行为均符合预期。这是违反直觉的陷阱。如果您坚持使用无符号常量,则必须编写,x & ~(uintmax_t)1因为x & ~1ULL如果x类型大于,甚至会失败unsigned long long。更糟糕的是,许多平台现在具有比更大的整数类型uintmax_t,例如__uint128_t
chqrlie

36

C和C ++通常在优化中使用“好像”规则。计算结果必须好像编译器没有优化您的代码。

在这种情况下,9/2*2=8。编译器可以使用任何方法来获得结果8。这包括位掩码,位移位或产生相同结果的任何特定于CPU的破解(x86的许多技巧都依赖于它不能区分指针的事实。和整数,与C和C ++不同)。


11
案例和重点。这就是GCC产生的-O1。所有方法都被提炼为相同的机器代码。
StoryTeller-Unslander Monica

而且我怀疑,对于所有3个变体,GCCand RAX, -2甚至都会在上使用该方法-O0
MSalters

7
GCC非常不寻常,因为即使禁用优化,它也会应用这类数学/位旋转窥视孔优化。这是一种有趣且引人注目的设计怪癖。其他编译器在禁用优化的情况下将C代码更直接地转换为机器指令,但是启用优化器后,输出将完全相同,这就是为什么应该编写代码以提高可读性,除非您确定知道自己的代码。编译器是死人。
科迪·格雷

1
将除法转换为班次是一种称为“强度降低”的标准优化技术。是的,它是非常标准的,而且非常简单,但是我不同意这不是优化。@supercat
Cody Gray

1
@supercat:我在这里大致同意科迪·格雷。强度降低不仅仅在于;这是事实,GCC不会为单个表达式生成代码。x/2*2编译为单个语句。通常,未经优化的编译是出于调试目的,然后您需要源代码和汇编之间的1:N对应关系。
MSalters

21

您可以编写,x / 2 * 2并且如果x具有无符号类型,则编译器将产生非常有效的代码来清除最低有效位。

相反,您可以这样写:

x = x & ~1;

或者可能不太可读:

x = x & -2;

甚至

x = (x >> 1) << 1;

还是这个:

x = x - (x & 1);

或由supercat建议的最后一个方法,适用于所有整数类型和表示形式的正值:

x = (x | 1) ^ 1;

以上所有建议对于2的补码体系结构上的所有无符号整数类型均正常工作。编译器是否会生成最佳代码是配置和实现质量的问题。

但是请注意,x & (~1u)如果类型x大于则不起作用unsigned int。这是违反直觉的陷阱。如果您坚持使用无符号常量,则必须编写,x & ~(uintmax_t)1因为x & ~1ULL如果x类型大于,甚至会失败unsigned long long。更糟糕的是,许多平台现在都具有大于的整数类型uintmax_t,例如__uint128_t

这是一个小基准:

typedef unsigned int T;

T test1(T x) {
    return x / 2 * 2;
}

T test2(T x) {
    return x & ~1;
}

T test3(T x) {
    return x & -2;
}

T test4(T x) {
    return (x >> 1) << 1;
}

T test5(T x) {
    return x - (x & 1);
}

T test6(T x) {  // suggested by supercat
    return (x | 1) ^ 1;
}

T test7(T x) {  // suggested by Mehrdad
    return ~(~x | 1);
}

T test1u(T x) {
    return x & ~1u;
}

正如Ruslan所建议的那样,在Godbolt的Compiler Explorer进行的测试表明,上述所有替代方案都为gcc -O1产生了完全相同的代码unsigned int,但是将类型更改Tunsigned long long显示了不同的代码test1u


以下是GCCx/2*2对未签名的操作的示例xgodbolt.org/g/Nee7cJ
Ruslan

@Ruslan:感谢您的评论,gcc确实为所有建议的替代方案生成了相同的代码。
chqrlie

@MM:我相信它适用于所有整数类型和表示形式,但仅适用于的正值x。您能证明它不适用什么价值吗?
chqrlie

1
@MM:怎么样?1为正,并且始终表示为00...01|1设置LSB。^1切换同一位。一个补码会影响MSB的值,INT_MIN但不会影响MSB的值。IOW,当您将算术和数值运算混合使用时,数字表示形式开始变得很重要,而我们不这样做。(但问题在于如何将负数四舍五入,向下舍入或舍入为零)
MSalters

1
如果没有大家的喜欢,模数运算的难题就是好!x = x - (x%2)?这具有可扩展到任何n地方的优势roundToMultiple(int x, int n) { return x - (x % n); }
corsiKa '17

7

如果您所说的值是任何无符号类型,最简单的方法是

x & -2;

无符号算术的奇妙之处在于将其-2转换为的类型x,并具有一个全为1的位模式,但最低有效位为0

与其他一些建议的解决方案相反,此方法应与至少等于的任何无符号整数类型一起使用unsigned。(而且,无论如何,您都不应该对较窄的类型进行算术运算。)

如supercat所述,额外的奖励仅使用将带符号类型转换为无符号类型。标准已将其定义为模算术。因此,结果始终UTYPE_MAX-1UTYPE的无符号类型x。特别是,它与用于签名类型的平台的符号表示无关。


4
仅仅为了避免任何混淆,可能值得注意的是,将-2转换为“ unsigned”将产生“ unsigned value”,将其添加到2时将产生0,无论系统是否使用二进制补码有符号数学。相比之下,如果一个人有一个补码系统,〜1等于-1,当转换为无符号时,将产生一个所有位都置1的值。
超级猫

@supercat,谢谢,是的。我试图将这些想法整合到我的答案中。
詹斯·古斯特

为使此安全性适用于小于unsigned int的无符号类型,您可以使用(1u * x)&
plugwash 17-10-19

6

到目前为止,我还没有提到的一种选择是使用模运算符。我认为这至少与您的原始代码片段一样代表您的意图,甚至可能更好。

x = x - x % 2

正如其他人所说的那样,编译器的优化器将等效地处理任何合理的表达式,因此请担心更清楚的内容,而不是您认为最快的内容。所有令人费解的答案都是有趣的,但是您绝对不应使用其中的任何一个代替算术运算符(假设动机是算术而不是微调)。


1
是的,编译器的优化器足够聪明:godbolt.org/g/U9zsiL
Bob__

1
尽管OP确实指定了无符号整数,但应注意的是,当您给它提供一个有符号整数时,以上内容可能会产生令人惊讶的结果。
Yakk-亚当·内夫罗蒙特

@Yakk在C中,模定义为x % d = x - x / d * d,因此我的代码段可以保证与x = x/2*2问题中提到的结果相同。假设x您担心自己会被否定(而不是对的否定替代2),这将给出OP可能想要的:if x=-7then x/2*2 == x - x%2 == -6
亚瑟·塔卡

但是,您确实有这种担心,而且我不确定,直到我检查了一下,这一事实确实抛出了我的论点,即它更容易阅读(在负数的情况下)。
亚瑟·塔卡

@ArthurTacca我认为即使x/2*2使用负整数,OP也很令人惊讶。因此,不确定如何避免使用负整数的人表现出令人惊讶的代码!
Yakk-Adam Nevraumont

-2

只需使用以下内容:

template<class T>
inline T f(T v)
{
    return v & (~static_cast<T>(1));
}

不要担心这是函数,编译器应最终将其优化为v和(〜1),且类型为1。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.