在C / C ++中进行无符号左移之前的掩盖是否过于偏执?


72

这个问题的动机是由我在C / C ++中实现加密算法(例如SHA-1),编写与平台无关的可移植代码以及彻底避免未定义的行为引起的

假设标准的加密算法要求您实现此目的:

b = (a << 31) & 0xFFFFFFFF

其中ab是无符号的32位整数。注意,在结果中,我们丢弃了最低有效32位以上的任何位。


作为第一个幼稚的近似值,我们可以假设int在大多数平台上该宽度为32位,因此我们可以这样写:

unsigned int a = (...);
unsigned int b = a << 31;

我们知道该代码不会在任何地方都起作用,因为int在某些系统上为16位宽,在其他系统上为64位,甚至可能为36位。但是使用stdint.h,我们可以使用以下uint32_t类型来改进此代码:

uint32_t a = (...);
uint32_t b = a << 31;

这样我们就完成了,对吧?这就是我多年以来的想法。... 不完全的。假设在某个平台上,我们有:

// stdint.h
typedef unsigned short uint32_t;

在C / C ++中执行算术运算的规则是,如果类型(例如short)比窄int,则将其扩展到int所有值都适合的范围,unsigned int否则。

假设编译器定义short为32位(有符号)和int48位(有符号)。然后这些代码行:

uint32_t a = (...);
uint32_t b = a << 31;

实际上将意味着:

unsigned short a = (...);
unsigned short b = (unsigned short)((int)a << 31);

请注意,a之所以被提升为,是int因为所有ushort(即uint32)都适合int(即int48)。

但是现在我们有一个问题:将非零位左移到有符号整数类型的符号位中是未定义的行为。发生此问题的原因是我们uint32被提升为int48-而不是提升为uint48(这里可以左移)。


这是我的问题:

  1. 我的推理正确吗,这在理论上是合理的问题吗?

  2. 是否可以忽略此问题,因为在每个平台上,下一个整数类型都是宽度的两倍?

  3. 是一个好主意,以正确地抵御这种病态情况下通过屏蔽预这样?:输入b = (a & 1) << 31;。(这在每个平台上都必定是正确的。但是,这可能会使对速度要求严格的加密算法慢于必要。)

澄清/修改:

  • 我会接受C或C ++或两者的答案。我想知道至少一种语言的答案。

  • 预屏蔽逻辑可能会损害位旋转。例如,GCC将以汇编b = (a << 31) | (a >> 1);语言编译为32位的位旋转指令。但是,如果我们预先屏蔽了左移,则新逻辑可能不会转换为位旋转,这意味着现在执行4个操作而不是1个。


1
不清楚你在问什么。也请每个问题一个问题。
πάνταῥεῖ

5
同意@πάνταῥεῖ。C和C ++也是不同的问题。选择其中一个,不要将C编译为C ++,反之亦然。
对于这个网站来说太老实了

13
“在左移之前掩盖”,而(a << 31) & 0xFFFFFFFF不是jibe。该代码在移位进行屏蔽。
chux-恢复莫妮卡

2
我认为这是一个现实的问题。更好的示例是16位版本而不是32位版本。我的回答是:您可以使用,31u然后a将其升级为uint48
user3528438 '16

1
@Nayuki:您可以使用一些typedef作为using my_uint_at_least32 = std::conditional_t<(sizeof(std::uint32_t) < sizeof(unsigned)), unsigned, std::uint32_t>;
Jarod42

Answers:


24

说到问题的C面,

  1. 我的推理正确吗,这在理论上是合理的问题吗?

我以前没有考虑过这个问题,但我同意您的分析。C<<根据提升后的左操作数的类型定义了运算符的行为,并且可以想象,int当该操作数的原始类型为时,整数提升会导致该运算符被(带符号)uint32_t。我不希望在任何现代机器上都能看到这种效果,但是我全都按照实际标准编程,而不是个人期望。

  1. 是否可以忽略此问题,因为在每个平台上,下一个整数类型都是宽度的两倍?

尽管实际上无处不在,但C不需要整数类型之间的这种关系。但是,如果您确定仅依赖于标准-也就是说,如果您正在努力编写严格符合标准的代码-那么您就不能依赖这种关系。

  1. 通过像这样预先掩盖输入来正确防御这种病理情况是一个好主意吗?:b =(a&1)<< 31;。(这在每个平台上都必定是正确的。但是,这可能会使对速度要求严格的加密算法变得比必要的慢。)

unsigned long保证该类型至少具有32个值位,并且在整数提升下它不能提升为任何其他类型。在许多常见平台上,它的表示形式uint32_t与完全相同,甚至可能是相同的类型。因此,我倾向于这样写表达式:

uint32_t a = (...);
uint32_t b = (unsigned long) a << 31;

或者,如果您a只需要在计算中作为中间值b,则将其声明为unsigned long开始。


2
好辩,约翰!我有一点担心-long至少32位。但是,在当今的许多系统上,它将恰好是64位。由于扩展了算法,这是否会使代码不必要地变慢?
Nayuki 2016年

19
@Nayuki,我认为问题与编写普遍正确的代码有关。当需要调整代码以提高特定硬件的性能时,通常需要编写考虑该硬件特定特性的代码。在某种程度上,此类代码包含了有关实现的假设-这就是重点-该代码不严格符合该标准。在非预期系统上,它可能不是最佳选择,甚至表现出UB。
约翰·布林格

1
@Nayuki我将在部署代码(例如./configure脚本或makefile)中进行这种修改,并且只有在您发现有迹象表明目标系统上的速度太慢时才进行处理。(不过,“太慢”取决于您自己:))
很少出现“莫妮卡在哪里” Needy'Oct11

1
我想肯定地说,假设任何平台上的任何体面的编译器都可以优化掩蔽,并且意识到在所使用的特定平台上这是无操作的,这是安全的。戴上面具,我说!
菲利普·哈格隆德

2
@Nayuki:编译器真的很擅长算术。当编译器检测到只u64需要您的低32位时,就没有理由不使用32位寄存器进行移位了。因此,首先编写正确的代码,然后检查生成的程序集。
Matthieu M.

20

问题1:移位之前进行屏蔽确实可以防止OP担心的不确定行为。

问题2:“ ...因为在每个平台上,下一个整数类型是宽度的两倍?” ->不。“下一个”整数类型可以小于2x甚至相同的大小。

对于具有的所有兼容C编译器,下面的定义都很好uint32_t

uint32_t a; 
uint32_t b = (a & 1) << 31;

问题3:uint32_t a; uint32_t b = (a & 1) << 31;预计不会产生执行掩码的代码-可执行文件中不需要-仅在源代码中。如果确实出现了掩码,则应该以更快的速度获得更好的编译器。

建议的那样,最好强调这些转变带来的无符号性。

uint32_t b = (a & 1U) << 31;

@John Bollinger很好的回答很好,详细说明了如何处理OP的特定问题。

一个普遍的问题是如何形成一个至少有n位数,一定符号度 不受令人惊讶的整数提升影响的数字-这是OP困境的核心。下面通过调用unsigned不更改值的操作来实现此目的-有效执行除类型问题以外的其他操作。乘积的宽度至少unsigneduint32_t。通常,铸造可能会缩小类型。除非确定不会变窄,否则需要避免铸造。优化编译器不会创建不必要的代码。

uint32_t a;
uint32_t b = (a + 0u) << 31;
uint32_t b = (a*1u) << 31;

4
我很想将其包装在宏中并附带解释它的注释。否则,您只是在要求下一位删除“无操作”的开发人员。
Plugwash

2
@plugwash也许有点像#define PROMOTE_AT_LEAST_UNSIGNED(x) ((x) + 0u)或不太冗长的像 PROMOTE_UNSIGNED
chux-恢复莫妮卡

11

这个问题中获得关于uint32 * uint32算术可能的UB的线索,以下简单方法应在C和C ++中起作用:

uint32_t a = (...);
uint32_t b = (uint32_t)((a + 0u) << 31);

整数常量的0u类型为unsigned int。这促进了除a + 0uuint32_tunsigned int,取其宽。因为类型具有等级int或更高级别,所以不再发生提升,并且可以在左操作数为uint32_t或的情况下应用移位unsigned int

最终uint32_t转换为只会抑制有关变窄转换的潜在警告(例如,是否int为64位)。

体面的C编译器应该能够看到添加零是无操作,这比看到无符号移位后的预屏蔽没有效果要轻。


10

为了避免不必要的提升,您可以将更大的类型与一些typedef一起使用,例如

using my_uint_at_least32 = std::conditional_t<(sizeof(std::uint32_t) < sizeof(unsigned)),
                                              unsigned,
                                              std::uint32_t>;

4
注意:为了清楚起见,此答案仅适用于C ++。
Nayuki 2016年

5
补充此C ++代码的交流想法:#if UINT32_MAX > UINT_MAX && UINT_MAX != -1 typedef uint32_t my_uint_at_least32; #else typedef unsigned my_uint_at_least32; #endif
chux-恢复莫妮卡

-1

对于这段代码:

uint32_t a = (...);
uint32_t b = a << 31;

要提升a为无符号类型而不是有符号类型,请使用:

uint32_t b = a << 31u;

<<运算符的两端均为无符号类型时,则适用6.3.1.8(C标准草案n1570)中的这一行:

否则,如果两个操作数都具有符号整数类型或都具有无符号整数类型,则将具有较小整数转换等级的操作数转换为具有较大等级的操作数的类型。


您描述的问题是由您引起的,31这是signed int type6.3.1.8中的另一行

否则,如果带符号整数类型的操作数的类型可以表示带无符号整数类型的操作数的所有值,则带无符号整数类型的操作数将转换为带符号整数类型的操作数的类型。

强制a升格为签名型


更新:

这个答案是不正确的,因为6.3.1.1(2)(强调我的):

...

如果一个int可以表示原始类型的所有值(对于位字段,受宽度限制),则该值将转换为int;否则,它将转换为unsigned int。这些被称为整数promotions.58)所有其他类型是由不变的整数 促销

和脚注58(重点为我):

58)整数提升仅适用于:作为常规算术转换的一部分,应用于某些参数表达式,一元+,-和〜运算符的操作数以及移位运算符的两个操作数(由它们各自指定)条款。

由于仅发生整数提升,而不发生普通的算术转换,因此使用31u不能保证如上所述a将其转换unsigned int为。


换班<<使用不同的规则。
chux-恢复莫妮卡

1
@Nayuki“否则,如果两个操作数都具有符号整数类型...”在“常规算术转换”下,其标题为“除非另有明确说明,否则...”,由于C11§6.5.7,该答案的前一半无效3。这篇文章的问题不是由于“导致您使用31 int类型的签名”引起的。此外,本文的CC ++性质使异常的整数促销概念变得复杂。
chux-恢复莫妮卡

1
左移位运算符的操作数不在经受常规算术转换的操作数之列。取而代之的是,该标准指定它们要进行整数提升,这只是标准算术转换的一部分,并且特别是不包括保证提升的无符号操作数的类型本身是无符号的。
约翰·布林格

1
@ user3528438,OP假定系统的int宽度为48位。在a << 31u这样的系统上评估表达式的过程中,整数提升被应用到两个操作数,导致左操作数在int原始类型为时被提升为(带符号)uint32_t。提升的左操作数的类型也是结果的类型,如果它是不能表示结果值的带符号类型,则将导致未定义的行为。在这种情况下,所讨论的表达式肯定会产生这样的结果。
约翰·布林格

1
@JohnBollinger你和chux是对的。我没有意识到通常的算术转换和整数提升(6.3.1.1(2)和脚注58)之间的区别。
user3528438 '16
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.