内置类型的性能:char vs short vs int vs. float vs. double

80

看到Alexandre C在另一个话题中的reply,我很好奇是否内置类型有性能差异:

char vs short vs int vs. float vs. double

通常我们不考虑这样的性能差异(如果有的话)在我们的实际项目中,但出于教育目的,我想知道这一点。可以提出一般性问题:

  • 整数算术和浮点算术之间是否存在性能差异?

  • 哪个更快?为什么更快?请解释一下。


3
个人资料和测量。使用大量迭代。 - Thomas Matthews
12
@Thomas Matthews: 这可以回答我的一个问题:哪个更快。但无法回答“为什么更快”的问题。 - Nawaz
当然,整数类型和浮点类型适用于非常不同的事情。我可以想到很少的情况下会同时考虑两者都可接受。 - aschepler
1
如果你正在为一个没有浮点运算单元的设备编程,那么将一个算法从浮点数转换为整数(使用适当的比例因子)可能值得牺牲精度和程序员时间。 - plugwash
说句实话:我用C语言编写了一个程序,其中包含许多算术运算和内存交换(太复杂了,无法在此处发布)。我使用不同的数据类型进行编译,差异非常小。在我的英特尔咖啡湖上,我对单个线程中的62亿(!)函数调用进行了以下执行时间测试(所有都是有符号的):char:12.76秒,short:12.31秒,int:11.87秒,long 11.86秒,long long 11.60秒。所有操作都在堆栈上进行。我认为,与CPU处理数据相比,从内存中移动的数据大小对性能的影响要大得多。 - Gunnar Bernstein
9个回答

135

浮点数与整数:

历史上,浮点运算可能比整数运算要慢得多。但在现代计算机上,情况已经不再是这样了(在某些平台上它可能略慢一些,但除非您编写完美的代码并针对每个周期进行优化,否则差异将被代码中的其他低效性所掩盖)。

在一些性能有限的处理器上,例如高端手机,浮点运算可能比整数运算稍微慢一些,但通常仅相差一个数量级(或更好),只要硬件支持浮点运算。值得注意的是,随着手机需要运行越来越多的通用计算工作负载,这种差距正在迅速缩小。

在极度性能有限的处理器上(便宜的手机和你的烤面包机),通常没有浮点硬件,因此需要在软件中模拟浮点运算。这很慢——比整数运算慢几个数量级。

但如我所说,人们希望他们的手机和其他设备表现得越来越像“真正的计算机”,而硬件设计师正在迅速加强FPU以满足这种需求。除非您追求每一个周期或者您正在为具有少量或没有浮点支持的极限CPU编写代码,否则性能差异对您来说并不重要。

不同大小的整数类型:

通常,CPU最快地操作它们本地字长的整数(关于64位系统也有一些注意事项)。在现代CPU上,32位操作通常比8位或16位操作更快,但这在不同的体系结构之间差异很大。还要记住,不能孤立考虑CPU的速度;它是复杂系统的一部分。即使对16位数字的操作比对32位数字的操作慢2倍,当您使用16位数字表示数据而不是32位数字时,可以将两倍的数据放入缓存层次结构中。如果这使得所有数据都来自缓存而不是频繁缓存未命中,则更快的内存访问将超过CPU较慢的操作。

其他注释:

向量化进一步偏向于更窄的类型(如float和8位/16位整数)——您可以在相同宽度的向量中执行更多的操作。然而,良好的向量代码很难编写,因此您不能轻易获得这种优势,需要进行大量的仔细工作。

为什么会有性能差异?

一个操作在CPU上是否快速实际上只受两个因素的影响:操作的电路复杂度和用户对操作快速性的需求(在合理范围内)。如果芯片设计师愿意向问题投入足够的晶体管,那么任何操作都可以变得很快。但是,晶体管需要花钱(或者说,使用大量晶体管会使您的芯片更大,导致每个晶圆上的芯片更少,产量更低,这会造成成本),因此芯片设计师必须平衡使用多少复杂度来处理哪些操作,并且他们根据(认为

                 high demand            low demand
high complexity  FP add, multiply       division
low complexity   integer add            popcount, hcf
                 boolean ops, shifts

高需求、低复杂度的操作在几乎任何CPU上都会很快:它们是易于处理的任务,每个晶体管所带来的最大用户收益。

高需求、高复杂度的操作将在昂贵的CPU上(如计算机中使用的那些)快速完成,因为用户愿意为此支付。然而,您可能不愿意为了让您的烤面包机具有快速的FP乘法而额外支付3美元,因此便宜的CPU会省略掉这些指令。

低需求、高复杂度的操作通常在几乎所有处理器上都很慢;没有足够的好处来证明它们的成本。

低需求、低复杂度的操作如果有人费心思去考虑,就会很快,否则就不存在。

进一步阅读:

  • Agner Fog维护一个漂亮的网站,其中有大量讨论低级性能细节的内容(并且具有非常科学的数据收集方法)。
  • 英特尔® 64和IA-32架构优化参考手册(PDF下载链接在页面的中间位置)也涵盖了许多这些问题,但它专注于一系列特定的体系结构。

在讨论操作码的时序/吞吐量“孤立”时,大多数数学运算仍然慢得多(例如排除MOV等)。不过我找不到以前用过的好的实证PDF了:( - user166390
15
我喜欢你的复杂度/需求表,这真是一个很好的总结方式。+1 - jalf
@pst:只有在考虑延迟时,延迟才是更有意义的度量标准,而吞吐量是更有意义的度量标准,现代非嵌入式CPU可以在每个周期内执行(至少)一个FP乘法和加法。 - Stephen Canon
非常正确--我试图强调这一点,但你表达得更好,即使它不是那么直接。 - user166390
太棒了!写得非常好,是我在这个话题上读过的最好的答案之一。甚至链接也很棒。 - Mecki

15

当然可以。

首先,这完全取决于所涉及的CPU架构。

然而,整数和浮点类型的处理方式非常不同,因此以下情况几乎总是成立:

  • 对于简单操作,整数类型速度很快。例如,整数加法通常只需要一个周期的延迟,整数乘法通常需要2-4个周期的时间,如果我没记错的话。
  • 浮点类型以前处理速度较慢。然而,在今天的CPU上,它们具有出色的吞吐量,并且每个浮点单元通常可以每个周期处理一次操作,从而导致与整数操作相同(或类似)的吞吐量。然而,延迟通常更差。浮点加法通常具有大约4个周期的延迟(相对于整数的1个周期)。
  • 对于某些复杂操作,情况有所不同,甚至是相反的。例如,FP除法的延迟可能比整数小,仅仅因为在两种情况下都实现操作都很复杂,但是在FP值上更常用,因此可能会花费更多的努力(和晶体管)来优化该情况。

在某些CPU上,双精度比浮点速度慢得多。在某些架构中,没有专用的双精度硬件,因此它们通过传递两个浮点大小的块来处理,从而导致您的吞吐量更差,并且延迟增加了一倍。在其他情况下(例如x86 FPU),两种类型都转换为相同的内部格式(80位浮点格式,在x86的情况下),因此性能相同。在其他情况下,浮点和双精度都有适当的硬件支持,但是由于浮点具有较少的位数,所以可以稍微快一点,通常会减少相对于双精度操作的延迟。

免责声明:所有提到的定时和特征都只是记忆中提取的。我没有查找任何信息,因此可能错误。:)

对于不同的整数类型,答案会因为CPU架构的不同而有很大差异。由于x86架构历史悠久且曲折,必须原生支持8、16、32位(现在还有64位)操作,并且它们基本上都一样快(它们使用的硬件基本相同,只需根据需要清零高位即可)。

然而,在其他CPU上,比int小的数据类型可能更加昂贵,需要更多的时间来加载/存储(将一个字节写入内存可能必须通过加载其所在的整个32位字并进行位掩码操作来更新寄存器中的单个字节,然后将整个字写回)。同样地,对于比int大的数据类型,某些CPU可能必须将操作分成两部分,分别加载/存储/计算低半部分和高半部分。

但是在x86上,答案大多数情况下并不重要。由于历史原因,CPU需要对每种数据类型都提供强大的支持。因此,您可能注意到的唯一区别是浮点运算具有更高的延迟(但吞吐量类似,因此如果编写正确的代码,它们并不会)。


12

我认为没有人提到整数提升规则。在标准C/C++中,不能对比int更小的类型执行任何操作。如果char或short恰巧比当前平台上的int小,则它们会被隐式提升为int(这是错误的主要来源)。编译器需要执行这种隐式提升,没有违反标准的方法。

整数提升意味着语言中不允许对比int更小的整数类型进行任何操作(如加法、位运算、逻辑运算等等)。因此,char/short/int的操作通常同样快,因为前两者会被提升为int。

除了整数提升之外,还有“通常算术转换”,即C语言会尽量使两个操作数成为相同的类型,并将它们中较小的一个转换为较大的那个。

然而,CPU可以在8、16、32等级别上执行各种加载/存储操作。在8位和16位体系结构上,这通常意味着8位和16位类型比整数提升后的类型更快。在32位CPU上,它实际上可能意味着较小的类型更,因为它想要将所有内容整齐地对齐到32位块中。32位编译器通常优化速度,并将较小的整数类型分配到比指定空间更大的空间中。

虽然通常较小的整数类型占用的空间比较大的类型要少,但如果您想要优化RAM大小,它们是更好的选择。


3
“编译器必须引发未定义行为”是什么意思?编译器对于具有未定义行为的代码不必进行任何操作或执行任何操作 :) - Jonathan Wakely
1
这意味着如果移位导致未定义行为将产生副作用,编译器不会“优化掉”该副作用,而是会调用它。例如,如果未定义的行为会导致数据更改,编译器可能会认为程序员依赖于这种情况发生。就标准而言,这当然超出了范围,但编译器是确定性的。“啊哈,程序员在这里期望程序崩溃。即将发生一次程序崩溃!” - Lundin
3
@Lundin 这完全不是真的。如果副作用源于未定义行为,优化时通常会将其消除。如果你认为你总能在期望的时候得到崩溃,那么你将面临令人不愉快的惊喜。未定义行为意味着任何事情都有可能发生。 - Jonathan Wakely
1
@JonathanWakely 很明显,这因情况而异。如果有符号整数溢出的未定义行为表现为某些二进制补码结果,那么可以非常安全地假设同一编译器在同一系统上每次都会确定性地执行此操作,尽管C标准允许其失控。无论如何,这些评论与原始主题无关,所以我会在这里停止。 - Lundin
3
@Lundin 不,这并不安全可靠。现代编译器的工作方式并非如此。检测溢出是否发生可能取决于优化级别、函数是否内联、调用该函数的上下文等许多变量。有很多变量参与其中,同一个编译器每次的操作也未必相同。 - Jonathan Wakely
显示剩余2条评论

8

上面的第一个答案非常好,我将其中一小块复制到了下面的重复内容(因为这是我最先看到的地方)。

“char”和“small int”比“int”慢吗?

我想提供以下代码,对各种整数大小进行分配、初始化和执行一些算术运算进行性能分析:

#include <iostream>

#include <windows.h>

using std::cout; using std::cin; using std::endl;

LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds;
LARGE_INTEGER Frequency;

void inline showElapsed(const char activity [])
{
    QueryPerformanceCounter(&EndingTime);
    ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedMicroseconds.QuadPart *= 1000000;
    ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;
    cout << activity << " took: " << ElapsedMicroseconds.QuadPart << "us" << endl;
}

int main()
{
    cout << "Hallo!" << endl << endl;

    QueryPerformanceFrequency(&Frequency);

    const int32_t count = 1100100;
    char activity[200];

    //-----------------------------------------------------------------------------------------//
    sprintf_s(activity, "Initialise & Set %d 8 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    int8_t *data8 = new int8_t[count];
    for (int i = 0; i < count; i++)
    {
        data8[i] = i;
    }
    showElapsed(activity);

    sprintf_s(activity, "Add 5 to %d 8 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    for (int i = 0; i < count; i++)
    {
        data8[i] = i + 5;
    }
    showElapsed(activity);
    cout << endl;
    //-----------------------------------------------------------------------------------------//

    //-----------------------------------------------------------------------------------------//
    sprintf_s(activity, "Initialise & Set %d 16 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    int16_t *data16 = new int16_t[count];
    for (int i = 0; i < count; i++)
    {
        data16[i] = i;
    }
    showElapsed(activity);

    sprintf_s(activity, "Add 5 to %d 16 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    for (int i = 0; i < count; i++)
    {
        data16[i] = i + 5;
    }
    showElapsed(activity);
    cout << endl;
    //-----------------------------------------------------------------------------------------//

    //-----------------------------------------------------------------------------------------//    
    sprintf_s(activity, "Initialise & Set %d 32 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    int32_t *data32 = new int32_t[count];
    for (int i = 0; i < count; i++)
    {
        data32[i] = i;
    }
    showElapsed(activity);

    sprintf_s(activity, "Add 5 to %d 32 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    for (int i = 0; i < count; i++)
    {
        data32[i] = i + 5;
    }
    showElapsed(activity);
    cout << endl;
    //-----------------------------------------------------------------------------------------//

    //-----------------------------------------------------------------------------------------//
    sprintf_s(activity, "Initialise & Set %d 64 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    int64_t *data64 = new int64_t[count];
    for (int i = 0; i < count; i++)
    {
        data64[i] = i;
    }
    showElapsed(activity);

    sprintf_s(activity, "Add 5 to %d 64 bit integers", count);
    QueryPerformanceCounter(&StartingTime);

    for (int i = 0; i < count; i++)
    {
        data64[i] = i + 5;
    }
    showElapsed(activity);
    cout << endl;
    //-----------------------------------------------------------------------------------------//

    getchar();
}


/*
My results on i7 4790k:

Initialise & Set 1100100 8 bit integers took: 444us
Add 5 to 1100100 8 bit integers took: 358us

Initialise & Set 1100100 16 bit integers took: 666us
Add 5 to 1100100 16 bit integers took: 359us

Initialise & Set 1100100 32 bit integers took: 870us
Add 5 to 1100100 32 bit integers took: 276us

Initialise & Set 1100100 64 bit integers took: 2201us
Add 5 to 1100100 64 bit integers took: 659us
*/

我的结果在i7 4790k上使用MSVC:

初始化并设置1100100个8位整数花费了:444微秒
将5添加到1100100个8位整数中花费了:358微秒

初始化并设置1100100个16位整数花费了:666微秒
将5添加到1100100个16位整数中花费了:359微秒

初始化并设置1100100个32位整数花费了:870微秒
将5添加到1100100个32位整数中花费了:276微秒

初始化并设置1100100个64位整数花费了:2201微秒
将5添加到1100100个64位整数中花费了:659微秒


2
整数算术和浮点算术之间是否存在性能差异?
是的。但这非常取决于平台和CPU。不同的平台可以以不同的速度执行不同的算术运算。
话虽如此,上述答复更具体些。pow()是一个通用例程,适用于双精度值。通过提供整数值,它仍在处理处理非整数指数所需的所有工作。直接乘法绕过了许多复杂性,这就是速度发挥作用的地方。这实际上并不是不同类型的问题,而是绕过大量复杂代码所需的问题,使pow函数适用于任何指数。

请回答“哪个更快,为什么?”。可以猜测“速度差异”,因为它们的表示方式不同。因此更有趣的事情是了解“为什么”? - Nawaz
@Nawaz:这真的取决于平台。很多问题都与你的架构的寄存器大小和数量有关(http://en.wikipedia.org/wiki/Processor_register)-如果你的CPU只有32位寄存器,那么`double`数学运算可能会很慢,因为它不能被存储在单个寄存器中。然而,32位整数可能会非常快。数字和类型会产生巨大的差异,但还有许多其他问题... 顺便说一句,在嵌入式系统工作中,你会看到这种情况更多,因为与通用桌面计算相比,嵌入式系统的限制要非常严格... - Reed Copsey
3
@Nawaz:你想深入了解多少?执行大多数浮点算术的逻辑电路比其整数对应物要复杂得多(当然,某些架构中可能有慢速整数ALU和快速FPU,因此复杂性可以用金钱来克服...有时候)。这是在非常低的层面上,然后在高层次上,这个答案相当清晰:你需要考虑更少的事情。对于你来说,计算x^2还是sqrt(x)更容易?pow(x,0.5)是一个平方根,比x^2所需的简单乘法更复杂。 - David Rodríguez - dribeas
@David:这是一个好评论。我认为你应该发布一个详细的答案,从逻辑电路层面一直解释到平方根! - Nawaz
2
@Nawaz:你需要的是一本书。SO并不适合提供像小说一样的答案。 - jalf

2

通常情况下,整数运算比浮点数运算更快。这是因为整数运算涉及到更简单的计算。然而,在大多数操作中,我们谈论的是少于十个时钟周期。不是毫秒、微秒、纳秒或滴答声;是现代核心中每秒发生2-30亿次的时钟周期。此外,自486以来,许多核心都有一组浮点处理单元或FPU,这些单元被硬连线以高效地执行浮点运算,并且通常与CPU并行。

由于这些原因,尽管从技术上讲它更慢,但浮点计算仍然非常快,任何试图计算差异的尝试都会在计时机制和线程调度中具有更多的误差,而不是实际执行计算所需的时间。在可以使用整数时,请使用整数,但要了解何时不能使用整数,并不要过于担心相对计算速度。


1
-1 不正确:"在大多数操作中,我们谈论的时钟数都不到十个。" 大多数现代x86 CPU可以在1-2个周期(整数和浮点数)内执行算术运算。 "自486以来,许多核心都有...FPU" - 实际上,自赛扬以来所有x86 CPU都具备FP硬件支持。 - sleske

1

这取决于处理器和平台的组成。

具有浮点协处理器的平台可能比整数算术慢,因为值必须在协处理器之间传输。

如果浮点处理在处理器核心内部进行,则执行时间可能可以忽略不计。

如果浮点计算由软件模拟,则整数算术将更快。

如果不确定,进行分析。

在优化之前,请确保编程正确且稳健。


0

不,其实并没有。当然这取决于CPU和编译器,但性能差异通常是可以忽略的,如果有的话。


3
取决于情况。在日常应用程序代码中往往可以忽略不计,但在高性能数字代码中,它可能会产生重大影响。我至少可以列举一种CPU,其中“double”加法比“int”加法慢14倍,这在涉及浮点计算的应用程序中肯定能够感受到 ;) - jalf

0

浮点数和整数算法之间的差异是肯定存在的。根据CPU的特定硬件和微指令,性能和/或精度也会有所不同。对于准确的描述,可以使用以下谷歌术语(我也不太确定):

FPU x87 MMX SSE

关于整数的大小,最好使用平台/架构的字长(或双倍),在x86上为int32_t,在x86_64上为int64_t。一些处理器可能具有处理多个这些值并行相加或相乘的内部指令(例如SSE(浮点数)和MMX),这将加快运算速度。


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