无符号整数与有符号整数的性能比较

93

使用无符号整数与有符号整数相比,是否存在性能收益或损失?

如果是这样,短整型和长整型也是如此吗?


18
@JeremyP,我想建议您仅针对大多数开发人员和应用程序说出了真相... - Brett
1
@Brett:大多数CPU上有符号和无符号算术的区别为零。除非你要进行大量算术运算,否则各种大小之间的差异微不足道。 - JeremyP
12个回答

121

使用无符号整数unsigned int进行2的幂次方除法更快,因为可以优化成单个移位指令。使用有符号整数signed int通常需要更多的机器指令,因为除法向零舍入,但右移则向下舍入。例如:

int foo(int x, unsigned y)
{
    x /= 8;
    y /= 8;
    return x + y;
}

以下是相关的 x 部分(带符号除法):

movl 8(%ebp), %eax
leal 7(%eax), %edx
testl %eax, %eax
cmovs %edx, %eax
sarl $3, %eax

这里是相关的y部分(无符号除法):

movl 12(%ebp), %edx
shrl $3, %edx

13
只有在除数是已知的、且为2的幂次方的编译时常量时,这才有效,对吗? - sharptooth
1
@sharptooth,对于除法,是的。可能还有其他位操作技巧,只适用于无符号或有符号数。我认为积极影响不仅限于一个方向。 - AProgrammer
为什么对于非常数除数无法执行该技巧?x86的shrl的第一个操作数应该是文字吗? - Manu343726
@Manu343726 如果除数不是2的幂怎么办?(即使是,你也必须先计算数字的二进制对数才能进行移位。) - fredoverflow
2
在现代流水线CPU架构中,更多的指令并不总是意味着运行时间更慢。也就是说,在得出深远结论之前,我仍然会进行测量。 - ulidtko

58

在C++(以及C)中,有符号整数溢出是未定义的,而无符号整数溢出则被定义为循环。请注意,例如在gcc中,您可以使用-fwrapv标志使有符号溢出定义为循环(即包装)。

未定义的有符号整数溢出允许编译器假设不会发生溢出,这可能会引入优化机会。请参见例如此博客文章进行讨论。


4
请注意,自 C++ 20 版本以来,情况已经不同。 - Seideun
1
@Seideun:这比那更加复杂。算术运算中的有符号整数溢出仍然是未定义的,因此 INT_MAX + 1 是未定义行为,INT_MIN % -1 也是如此。发生了什么变化呢?现在对于任何非不确定值,signed_integer_type(any_integer_value) 都已被定义,并保证给出二进制补码结果。 - David Stone

28

unsigned会比signed具有相同或更好的性能。以下是一些例子:

  • 除以2的幂次方这样的常数(请参阅FredOverflow的答案)
  • 除以一个常数(例如,我的编译器用2个汇编指令实现对无符号数除以13,而对于有符号数则需要6个指令)
  • 检查一个数是否为偶数(我不知道为什么我的MS Visual Studio编译器用4个指令来实现signed类型的数字,而gcc用1个指令,就像在unsigned案例中一样)

short通常比int具有相同或更差的性能(假设sizeof(short) < sizeof(int))。当你将算术运算的结果(通常是int,而不是short)分配给存储在处理器寄存器中的short类型变量(也是int类型)时,性能下降会发生。所有从shortint的转换都需要时间,很烦人。

注意: 一些DSP具有针对signed short类型的快速乘法指令;在这种特定情况下,shortint更快。

至于intlong之间的区别,我只能猜测(我不熟悉64位体系结构)。当然,如果intlong大小相同(在32位平台上),它们的性能也相同。


几个人指出的一个非常重要的补充:

对于大多数应用程序,真正重要的是内存占用和使用带宽。您应该使用最小必需的整数(如short,甚至signed/unsigned char)来处理大型数组。

这将会提供更好的性能,但是收益是非线性的(即不是2或4的倍数),并且有些不可预测——它取决于您的应用程序中缓存大小以及计算和内存传输之间的关系。


11
关于short与int性能比较的说法,我会谨慎一些。虽然使用int进行算术运算可能会更快,但要记住,在现代桌面CPU上,整数运算很少成为瓶颈,而另一方面,内存带宽通常是瓶颈,因此对于大型数据集,short可能实际上会比int提供更好的性能。此外,对于自动向量化的代码,使用较小的数据类型通常意味着可以处理更多的数据元素,因此即使算术性能可能会增加(尽管在当前自动向量化器的状态下不太可能)。 - Grizzly
1
@Grizzly 我同意(我的应用程序实际上是计算密集型的,所以我对short的经验与你/任何其他人不同) - anatolyg
2
@martinkunev 当然可以!这可能是今天使用“short”的唯一原因(非缓存RAM事实上是无限的),也是一个非常好的理由。 - anatolyg
3
我理解您的意思是当受限于内存时,“short”比“int”更快。但是根据我的经验,在x86架构下它们的性能相同,而在ARM架构下,“short”会更慢一些。 - anatolyg
1
@anatolyg 关于向量化; 我实际上是指MMX...我曾经错误地认为它是唯一支持打包字大小整数操作的扩展集; 我认为SSE及其以上版本仅针对int数据在DW和QW上运行。刚刚拿出i-set参考资料检查,意识到我非常错误。每个SSE都有新的W和B整数操作来增强原始的MMX。我真的应该更仔细地阅读这一部分,但是为了辩护,新向量指令的数量足以让任何人的大脑受伤。我的错误!评论已撤回。 - Josh Parnell
显示剩余5条评论

18

这将取决于具体的实现方式。在大多数情况下,不会有任何区别。但如果您真的关心,您必须尝试所有您考虑的变体并测量性能。


24
如果你想知道,你就需要去衡量。这个问题几乎每周都要回答,非常烦人。 - sbi
如果您正在尝试编写可移植的代码,测量并不能回答您的问题。 - martinkunev
如果您需要可移植的代码,则需要在您想要移植到的所有平台上进行测量 - 但结果很可能是一个平台的优化是另一个平台的劣化。在我看来,这主要归结为:要么您将优化定制到平台上,要么您不进行优化。 - sbi

9
无符号整数与有符号整数之间的性能差异实际上比接受答案所建议的更为普遍。将无符号整数除以任何常数可以比将有符号整数除以常数更快,而不管常数是否是 2 的幂。参见http://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html
在帖子的结尾,他包括以下部分:
一个自然的问题是相同的优化是否可以改进有符号除法;不幸的是,由于两个原因,它似乎并没有:
被除数的增量必须成为数量的增加,即如果 n > 0,则增加,如果 n < 0,则减少。这引入了额外的开支。
对于不合作的除数,有符号除法的惩罚仅约为无符号除法的一半,留下更小的改进空间。
因此,似乎可以让向下舍入算法在有符号除法中工作,但将表现不及标准向上舍入算法。

9
这在很大程度上取决于具体的处理器。
在大多数处理器上,有符号算术和无符号算术都有相应的指令,因此使用有符号整数和无符号整数之间的区别取决于编译器使用哪种类型。
如果其中任何一种更快,则完全取决于特定的处理器,并且如果存在差异,则很可能微不足道。

5

无符号类型不仅在2的幂次方除法中更快,而且在除以其他任何值时也更快。如果您查看Agner Fog's Instruction tables,您会发现无符号除法与有符号版本具有类似或更好的性能

例如,在AMD K7上

指令 操作数 操作码 延迟周期 吞吐率倒数
DIV r8/m8 32 24 23
DIV r16/m16 47 24 23
DIV r32/m32 79 40 40
IDIV r8 41 17 17
IDIV r16 56 25 25
IDIV r32 88 41 41
IDIV m8 42 17 17
IDIV m16 57 25 25
IDIV m32 89 41 41

同样的事情适用于英特尔奔腾处理器

指令 操作数 时钟周期
DIV r8/m8 17
DIV r16/m16 25
DIV r32/m32 41
IDIV r8/m8 22
IDIV r16/m16 30
IDIV r32/m32 46

当然,这些都是相当古老的。具有更多晶体管的新型架构可能会缩小差距,但基本的事情仍然适用:通常需要更多的微操作、更多的逻辑、更多的延迟才能进行带符号除法。


4
简而言之,事前不必费心,但事后需要费心。
如果你想要性能,你必须使用编译器的性能优化,这可能与常识相反。请记住,不同的编译器可以将代码编译成不同的形式,并且它们本身也具有不同类型的优化。如果我们谈论一个 g++ 编译器,并使用 -Ofast 或至少 -O3 标志来达到最高优化级别,根据我的经验,它可以将 long 类型编译成比任何 unsigned 类型或仅仅是 int 类型性能更好的代码。
这是我自己的经验,并建议你先编写完整的程序,只有在拥有实际代码并可以使用优化进行编译以尝试选择实际执行效果最好的类型时才关注这些问题。这也是关于代码优化性能的非常通用的建议,首先快速编写,尝试使用优化进行编译,调整以查看哪种方法最有效。你还应该尝试使用不同的编译器编译程序,并选择输出最高效机器代码的编译器。
一个经过优化的多线程线性代数计算程序与未经优化的程序相比,性能可以轻松提高10倍以上。所以这很重要。
在许多情况下,优化器的输出与逻辑相矛盾。例如,我曾经遇到这样一个情况:将 a[x]+=ba[x]=b 区别开来,会使程序执行时间快了近2倍。而不,a[x]=b 并不是更快的那个。
例如,NVidia声明 为了编写他们的GPU:
注意:正如已经推荐的最佳实践一样,在SMM上应该优先选择有符号算术,以获得最佳吞吐量。C语言标准对无符号数学的溢出行为施加了更多限制,从而限制了编译器的优化机会。

1

传统上,int 是目标硬件平台的本机整数格式。任何其他整数类型可能会导致性能损失。

编辑:

现代系统略有不同:

  • 出于兼容性原因,在 64 位系统上,int 实际上可能是 32 位。我相信这在 Windows 系统上发生。

  • 现代编译器在某些情况下隐式地使用 int 来执行较短类型的计算。


是的,传统上来说 ;-) 在当前的64位系统中,int仍然是32位宽度,但64位类型(根据操作系统而定的longlong long)应该至少与其速度相当。 - Philipp
2
在我所知道的所有系统上(Windows,Linux,Mac OS X),int类型始终是32位宽度,无论处理器是否为64位。而long类型是不同的:在Windows上是32位,在Linux和OS X上是一个字。 - Philipp
@Philipp 但是 int 不一定始终为32位宽。 - mercury0114
@mercury0114:对于性能调优的假设,可以安全地假设int是CPU可以高效操作的类型。例如,在大多数64位平台上使用32位,因为它们都具有高效的32位整数操作,特别是x86-64,其中int 本机整数格式,在机器代码中是默认的操作数大小。(完整的寄存器宽度为64位,但使用它需要每个指令额外的一个字节的机器代码)。通常int也不会太大;很少是64位。但当然,在某些系统上实际上确实是这样,我记得一些旧的Cray计算机就是这样。 - Peter Cordes
简而言之:评论让人误以为“int”作为32位的“兼容性”会影响性能。实际上并不会。32位也是性能最佳的选择,特别是在x86-64上(优势比在AArch64上少,因为“uint64_t”不需要额外的代码大小)。请参阅在x86-64中使用32位寄存器/指令的优点 - Peter Cordes

1

据我所知,在x86上,有符号/无符号不应该有任何区别。然而,短整型和长整型则是另一回事,因为对于长整型,需要移动到/从RAM的数据量更大(其他原因可能包括强制类型转换,如将短整型扩展为长整型)。


1
还要记住,某些编译器可能具有不适用于所有整数类型的优化。例如,至少旧的英特尔编译器如果for循环计数器不是signed int,则无法应用自动向量化。 - CAFxX
在编程中,指令级别并不重要,但从C++级别来看确实很重要。 - phuclv
@LưuVĩnhPhúc,你是在说有符号溢出是UB吗?如果是这样,我所知道的唯一一个重要情况是当优化编译器难以推理用作循环计数器/归纳变量的无符号整数时(这在我的评论中立即跟在你的评论之后)。 - CAFxX
不,还有其他情况需要考虑符号。你看了其他答案吗? - phuclv
我做了。你呢?他们中的大多数人说,除了编译时常量除法和循环归纳变量(我在评论中提到的)之外,并没有太大的区别。即使在你的例子中,你也有点指出在新处理器中差异并不是很大(例如,请查看Sandy Bridge表)。 - CAFxX

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