在32位CPU上,'整型'类型比'短整型'类型更有效吗?

14

在32位CPU上,整数占4个字节,短整数占2个字节。如果我编写一个使用许多数字值的C/C++应用程序,这些数字值始终适合于提供的短整数范围内,那么使用4字节整数还是2字节整数更有效率呢?

有人建议使用4字节整数更有效率,因为它符合从内存到CPU的总线带宽。但是,如果我将两个短整数相加,CPU是否会在单次传输中并行处理这两个值(从而跨越总线的4个字节带宽)?


重复的问题。请参见 .NET Integer vs Int16?(虽然标签是 .Net,但它适用于硬件架构相同的情况)。 - Jon Adams
7
@JonAdams:这完全不是任何重复,因为.NET是一个独立的框架,适用于.NET的内容可能不适用于其他任何东西。在某些CPU上,32位操作可能在.NET上更快(因为.NET已经针对此进行了优化),但是在编写纯C代码时,64位操作可能比32位操作在该CPU上更快(因为C编译器可以更好地为64位进行优化而不是32位)。 - Mecki
8个回答

20

如果你有一个大数组,那么就选择最小的适用尺寸。使用16位的short数组比32位的int数组更加高效,因为可以获得两倍的缓存密度。与在32位寄存器中使用16位值所需的任何符号扩展相比,CPU必须执行的成本微不足道,而缓存未命中的成本则非常高。

如果你只是在类中混合使用成员变量和其他数据类型,那么情况就不太清楚了,因为填充要求很可能会消除16位值的任何节省空间的好处。


15

在32位CPU上,您应该使用32位整数,否则它可能会遮盖未使用的位(即它将始终以32位进行计算,然后将答案转换为16位)。

它不会为您执行两个16位操作,但如果您自己编写代码并且确信它不会溢出,您可以自己执行操作。

编辑:我应该补充说明,这也在某种程度上取决于您对“高效”的定义。虽然它能够更快地执行32位操作,但您当然会使用两倍的内存。

如果这些用于某个内部循环中的中间计算,则应使用32位。 然而,如果您从磁盘读取此内容,或者即使只需要支付缓存未命中的代价,仍然可能更好地使用16位整数。与所有优化一样,只有一种方法可以知道:进行性能分析


2
需要注意的是,在C99标准的stdint.h中,有int_fastN_t和uint_fastN_t类型,其中N分别为8、16、32和64(尽管并非所有类型始终都可用)。在C++中,boost库也有对应的类型定义。同时,g++编译器通常会包含stdint.h头文件。这些类型是被认为是最快速度,满足所需最小尺寸的数据类型。 - Evan Teran
@EvanTeran:但是最快用于什么目的/用例?局部变量与大数组不同。对于x86-64,glibc不幸选择了64位int_fast16_t和32位,使它们成为许多事情的糟糕选择。可能设计师们考虑的是不需要为数组索引进行符号扩展,但在Ice Lake之前,Intel上的64位除法速度较慢,而在许多早期的x86-64 CPU(包括AMD)上,64位乘法速度也较慢。请参见为什么在x86_64中uint_least16_t比uint_fast16_t更快进行乘法运算? - Peter Cordes
此外,为什么快速整数类型比其他整数类型更快? / [应该如何为x86_64定义[u]int_fastN_t类型,使用或不使用x32 ABI?](应该如何为x86_64定义[u]int_fastN_t类型,使用或不使用x32 ABI?). 此外,SIMD自动向量化意味着数组中的窄元素对于计算和内存带宽都很好。(如果需要解包,则可能是坏的。) - Peter Cordes
@PeterCordes 我不能确定,但我猜测答案是“对于单个访问和使用最快”。一旦你开始谈论SIMD和大量的数组,当然,对于这种用例,你需要更多地考虑你特定的用例。 - Evan Teran

8
如果您正在使用“许多”整数值,那么处理的瓶颈可能是内存带宽。 16位整数可以更紧密地打包到数据缓存中,因此可以提高性能。
如果您需要对大量数据进行数字计算,则应阅读Ulrich Drepper的《每个程序员都应了解的内存知识》。请重点关注第6章,了解如何最大化数据缓存的效率。

5
32位CPU通常在内部处理32位值,但这并不意味着在处理8/16位值时速度会变慢。例如,x86向后兼容到8086,可以操作寄存器的一部分。这意味着即使寄存器宽度为32位,它也可以仅在寄存器的前16或前8位上操作,而不会有任何减速。这个概念甚至被x86_64采用,其中寄存器是64位的,但它们仍然可以只在前32、16或8位上操作。
此外,x86 CPU总是从内存中加载整个高速缓存行(如果未在缓存中),而高速缓存行比4字节大(对于32位CPU而言,为8或16字节),因此从内存加载2字节与加载4字节同样快。如果从内存中处理许多值,则16位值实际上可能比32位值快得多,因为内存传输较少。如果高速缓存行为8字节,则每个高速缓存行有四个16位值,但只有两个32位值,因此使用16位int时,每四个值需要一次内存访问,使用32位int时,每两个值需要一次内存访问,处理大型int数组时会导致传输量增加一倍。
其他CPU(如PPC)不能仅处理寄存器的一部分,它们总是处理完整的寄存器。然而,这些CPU通常具有特殊的加载操作,允许它们例如从内存中加载16位值,将其扩展为32位并写入寄存器。稍后它们有一个特殊的存储操作,它从寄存器中取出该值,并仅将最后的16位存回内存;这两个操作只需要一个CPU周期,就像32位加载/存储需要的那样,因此也没有速度差异。由于PPC只能在寄存器上执行算术运算(与x86不同,后者也可以直接在内存上操作),所以无论使用32位int还是16位int,这种加载/存储过程都会发生。
唯一的劣势是,如果在只能处理完整寄存器的32位CPU上链接多个操作,则下一个操作执行之前,最后一个操作的32位结果可能必须“切回”到16位,否则结果可能不正确。这样的切换只需要一个CPU周期(一个简单的AND操作),编译器非常擅长确定何时真正需要这样的切换,何时省略不会对最终结果产生任何影响,因此这样的切换不会在每个指令后执行,只有在确实无法避免时才会执行。一些CPU提供了各种“增强”指令,使这样的切换变得不必要。我在生活中看到过很多代码,我本以为会有这样的切换,但是查看生成的汇编代码后,编译器找到了完全避免它的方法。
如果你期望有一个普遍规律,我必须让你失望。不能确定16位操作与32位操作的速度是否相同,也没有人能确定32位操作始终更快。这还取决于你的代码对这些数字的具体操作方式以及如何进行操作。我见过一些基准测试,在某些32位CPU上,32位操作比使用16位操作的同样代码更快,但我也见过相反的情况。即使从一个编译器切换到另一个编译器或升级编译器版本,也可能会改变一切。我只能说以下内容:声称使用short类型比使用int类型慢得多的人,请提供一个用于支持该主张的源代码示例,并说明用于测试的CPU和编译器版本,因为在过去的大约10年中,我从未经历过这样的情况。可能有一些情况下,使用int类型可能会快1-5%,但低于10%的差距并不是“显著的”,问题是,只为了买下2%的性能优势而浪费两倍的内存,是否值得?我认为不值得。

2
x86处理器在混合使用16位和32位操作时,如果你先写入ax再尝试从eax读取数据,会出现“部分寄存器停顿”的情况。这是需要注意的问题。 - cHao
另外,x86有类似的指令可以将一个短整型值读入到长寄存器中(movsxmovzx)。因此,您可以完全运行32位程序,避免停顿,并仍然使用16位值。 - cHao
1
@cHao:除了内存停顿,32位比16位更容易受到影响。当对整数进行大量计算且编译器可以将它们全部放置在寄存器中时,在x86/x86_64和PPC上,32位确实比16位快一点(尽管只有几个百分点)。然而,当对存储在数组中的许多(数百万)整数进行大量计算时,int16与int32的速度相同,有时甚至略快。上周末我在Intel Core 2 Duo和Motorola PPC G4上进行了基准测试,因为我想自己知道。有趣的是:所有CPU上的所有测试都在使用int8时最快。 - Mecki

3

不要听别人的建议,试试看。

这可能与您使用的硬件/编译器密切相关。一个快速测试可以轻松解决这个问题。可能花费的时间比在这里写问题还要少。


3
这要看情况。如果你的CPU负载较高,32位操作在32位CPU上比16位操作快。如果你的内存负载较高(特别是如果有太多的L2缓存未命中),那么使用尽可能小的数据。
你可以使用像Intel's VTune这样测量CPU和L2缓存未命中的分析器来找出你正在使用哪个。你将运行应用程序两次并使用相同的负载,它将合并两次运行以查看应用程序热点的一个视图,并且您可以看到每行代码上花费了多少周期。如果在昂贵的代码行中,您看到0个缓存未命中,则表示CPU负载较高。如果您看到大量未命中,则表示内存负载较高。

1

当你说32位时,我会假设你指的是x86。16位算术运算非常慢:操作数大小前缀使解码变得非常慢。因此,不要将临时变量设置为short int或int16_t。

然而,x86可以有效地将16位和8位整数加载到32位或64位寄存器中(movzx / movsx:零扩展和符号扩展)。因此,可以放心地在数组和结构体字段中使用short int,但请确保在临时变量中使用int或long。

然而,如果我正在将两个短整数相加,CPU是否会在单个并行传递中打包这两个值(从而跨越总线的4字节带宽)?

那是无稽之谈。加载/存储指令与L1缓存交互,限制因素是操作数的数量;宽度是无关紧要的。例如,在core2上:每个周期1次加载和1次存储,无论宽度如何。 L1缓存具有到L2缓存的128位或256位路径。

如果加载是瓶颈,则可以使用一次宽加载,在加载后使用移位或掩码进行拆分。或者使用SIMD在加载并行处理数据而无需在并行加载后解包。


1

如果你正在处理大型数据集,最大的问题就是内存占用。在这种情况下,一个好的模型是假设CPU速度无限快,并花费时间担心需要移动多少数据到/从内存中。事实上,现在的CPU速度非常快,有时候编码(例如压缩)数据更加高效。这样,CPU会做更多的工作(解码/编码),但内存带宽大大降低。

因此,如果你的数据集很大,最好使用16位整数。如果你的列表已排序,你可以设计一种差分或游程编码的编码方案,这将进一步减少内存带宽。


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