无符号整数和有符号整数之间的性能差异是什么?[关闭]


42

我知道将带符号的整数与浮点数混合时性能会下降。

将无符号整数与浮点数混合会更糟吗?

混合带浮点的有符号/无符号时是否有任何击中?

不同的尺寸(u32,u16,u8,i32,i16,i8)对性能有影响吗?在哪些平台上?


2
我删除了PS3专用的文本/标签,因为这是关于任何体系结构的一个好问题,并且答案对分离整数和浮点寄存器的所有体系结构都是正确的,实际上是所有这些。

Answers:


36

混合int(任何类型)和float的巨大代价是因为它们位于不同的寄存器集中。要从一个寄存器集转到另一个寄存器,您必须将该值写到内存中并读回,这会导致加载命中存储停顿。

在不同大小或整数的正负号之间移动会使所有内容保持在同一寄存器集中,因此避免了大笔费用。由于符号扩展等原因,可能会有较小的罚款,但这些罚款要比加载命中商店小得多。


您链接的文章指出,PS3单元处理器是一个例外,因为显然所有内容都存储在同一组寄存器中(可以在文章的中间找到或搜索“单元”)。
bummzack,2011年

4
@bummzack:这仅适用于SPE,不适用于PPE;SPE具有非常非常特殊的浮点环境,并且转换仍然相对昂贵。同样,有符号和无符号整数的成本仍然相同。

这是一篇很好的文章,了解LHS非常重要(我对此表示赞成),但我的问题是关于那些与标志相关的处罚。我知道这些内容很小,可能可以忽略不计,但是我仍然希望看到一些有关它们的真实数字或参考。
路易斯

1
@Luis-我正在尝试找到一些与此相关的公共文档,但目前找不到。如果您可以访问Xbox360文档,那么布鲁斯·道森(Bruce Dawson)会提供一份不错的白皮书,其中涵盖了其中的一些内容(并且总体而言非常好)。
celion

@Luis:我在下面发布了一个分析,但是如果您满意,请给celion答案-他说的都是正确的,我所做的只是运行GCC几次。

12

我怀疑有关Xbox 360和PS3的信息将专门用于仅允许开发人员使用的墙壁,例如大多数低级详细信息。但是,我们可以构造一个等效的x86程序并将其反汇编以得到一个总体思路。

首先,让我们看看未签名的扩展费用:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

相关部分分解为(使用GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

因此基本上相同-在一种情况下,我们移动一个字节,在另一种情况下,我们移动一个字。下一个:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

变成:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

因此,符号扩展的成本是子指令级别movsbl而不是成本movzbl。由于现代处理器的工作方式,这基本上不可能在现代处理器上进行量化。从内存速度到缓存再到流水线之前的一切,其他一切都将主导运行时。

在大约10分钟的时间里,我编写了这些测试,我很容易找到了一个真正的性能错误,而且,只要打开任何级别的编译器优化,对于这些简单的任务,代码就变得无法识别。

这不是堆栈溢出,所以我希望这里没有人会声称微优化无关紧要。游戏通常会处理非常大且非常数字化的数据,因此,对分支,强制转换,调度,结构对齐等方面的注意会带来非常关键的改进。任何花了很多时间来优化PPC代码的人都可能至少有一个关于负载打击商店的恐怖故事。但是在这种情况下,这真的没有关系。整数类型的存储大小只要对齐并适合寄存器,就不会影响性能。


2
(CW,因为这实际上只是对celion答案的评论,并且因为我很好奇人们可能必须进行哪些代码更改才能使其更具说明性。)

有关PS3 CPU的信息可随时获得,并且可以合法获得,因此,讨论与PS3相关的CPU问题不成问题。在索尼取消对OtherOS的支持之前,任何人都可以将Linux粘贴到PS3上并对其进行编程。GPU超出了限制,但是CPU(包括SPE)很好。即使没有OtherOS支持,您也可以轻松地获取适当的GCC,并查看代码源是什么样的。
JasonD

@Jason:我已将我的职位标记为CW,因此如果有人这样做,他们可以提供信息。但是,任何有权使用Sony官方GameOS编译器(实际上是唯一重要的编译器)的人都可能被禁止这样做。

实际上,在PPC IIRC上,带符号整数会更昂贵。它确实对性能造成了很小的影响,但是它确实存在。。。这里还有很多PS3 PPU / SPU的详细信息:jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html和此处:jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html。好奇这个GameOS编译器是什么吗?是GCC编制者还是SNC编制者?除了已经提到的内容以外,iirc在讨论优化最内层循环时还有开销。我没有访问描述此内容的文档-即使我做了...
jheriko 2012年

4

在几乎所有架构上,带符号整数运算的开销都可能更高。例如,用无符号表示的常量除法更快,例如:

unsigned foo(unsigned a) { return a / 1024U; }

将被优化为:

unsigned foo(unsigned a) { return a >> 10; }

但...

int foo(int a) { return a / 1024; }

将优化为:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

或在分支价格便宜的系统上,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

模也一样。非2的幂次也是如此(但示例更复杂)。如果您的体系结构没有硬件划分(例如,大多数ARM),则非const的无符号划分也将更快。

通常,告诉编译器不会产生负数将有助于表达式的优化,尤其是用于循环终止和其他条件的表达式。

至于不同的int大小,是的,这会产生轻微的影响,但是您必须权衡一下,而不是移动较少的内存。如今,访问较少的内存可能比增加大小所损失的更多。那时,您还需要进行微优化。


我编辑了优化的代码,以更好地反映GCC实际生成的内容,即使在-O0上也是如此。当test + lea让您无分支地进行分支时,会产生误导。

2
在x86上,也许。在ARMv7上,它只是有条件地执行。
约翰·里普利

3

使用带符号或无符号int的操作在当前处理器(x86_64,x86,powerpc,arm)上具有相同的成本。在32位处理器上,u32,u16,u8,s32,s16,s8应该相同。您可能会因为对准不良而受到惩罚。

但是将int转换为float或将float转换为int是一项昂贵的操作。您可以轻松找到优化的实现(SSE2,Neon ...)。

最重要的一点可能是内存访问。如果您的数据不适合L1 / L2缓存,那么您将失去比转换更多的周期。


2

乔恩·普迪(Jon Purdy)上面说(我无法评论),未签名可能会更慢,因为它不会溢出。我不同意,无符号算术是对单词中位数进行模2的简单模运算。原则上,已签名的操作可能会发生溢出,但是通常会将其关闭。

有时,您可以做一些聪明的(但不是很容易理解的事情),例如将两个或多个数据项包装到一个int中,并且每条指令获得多个操作(口袋算术)。但是您必须了解自己在做什么。当然,MMX可以让您自然地做到这一点。但是有时使用最大的硬件支持的字大小并手动打包数据可为您提供最快的实现。

注意数据对齐。在大多数硬件实现中,未对齐的加载和存储速度较慢。自然对齐意味着对于一个4字节的字,地址是4的倍数,而8字节的字地址应该是8字节的倍数。这会延续到SSE(128位有利于16字节对齐)。AVX很快将把这些“向量”寄存器的大小扩展到256位,然后是512位。并且对齐的加载/存储将比未对齐的加载/存储更快。对于硬件极客,未对齐的内存操作可能会跨越诸如高速缓存行甚至页面边界之类的事情,硬件必须对此加以注意。


1

对于循环索引,最好使用带符号的整数,因为带符号的溢出在C中是未定义的,因此编译器将假定此类循环的拐角情况较少。这是由gcc的“ -fstrict-overflow”(默认启用)控制的,如果不读取程序集输出,效果可能很难注意到。

除此之外,如果您不混合使用类型,x86会更好地工作,因为它可以使用内存操作数。如果必须转换类型(符号扩展或零扩展),则意味着显式加载和使用寄存器。

坚持使用int作为局部变量,默认情况下大多数都会发生。


0

正如celion所指出的,在int和float之间转换的开销主要与寄存器之间值的复制和转换有关。无符号int及其自身的唯一开销来自其有保证的环绕行为,这需要在编译后的代码中进行一定量的溢出检查。

在有符号和无符号整数之间进行转换基本上没有开销。根据平台的不同,整数的大小可能(无限地)更快或更慢。一般而言,最接近平台字大小的整数大小将是访问速度最快的但是整体性能差异取决于许多其他因素,尤其是缓存大小:如果uint64_t在需要时使用uint32_t,则可能因为较少的数据会立即放入缓存中,并且可能会产生一些负载开销。

但是,甚至考虑这个问题也有些过分。如果您使用适合于数据的类型,则应该可以很好地工作,并且通过基于体系结构选择类型所获得的功能可以忽略不计。


您指的是什么溢出检查?除非您的意思是比汇编程序低的级别,否则在大多数系统上添加两个int的代码是相同的,而在少数使用例如符号大小的系统上,该代码实际上不会更长。只是不同。

@JoeWreschnig:该死。我似乎找不到它,但是我知道我已经看到了至少在某些平台上考虑了定义的环绕行为的不同汇编器输出的示例。唯一的相关文章中,我能找到的:stackoverflow.com/questions/4712315/...
乔恩·珀迪

对于不同的环绕行为,不同的汇编器输出是因为编译器可以在有符号的情况下进行优化,例如,如果b> 0则a + b> a,这是因为未定义有符号的溢出(因此无法依赖)。确实是完全不同的情况。
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.