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


72

这似乎是一个愚蠢的问题,但是看到Alexandre C在另一个主题中的答复,我很好奇知道内置类型是否存在性能差异:

charVS shortVSint主场迎战float 主场迎战double

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

  • 积分算术和浮点算术之间是否有性能差异?

  • 哪个更快?更快的原因是什么?请解释一下。


3
剖析并测量。使用大量的迭代。
Thomas Matthews

10
@Thomas Matthews:可以回答我的一个问题:更快。但不是“为什么更快”。
纳瓦兹

加上当然,整数类型和浮点类型对于完全不同的事物很有用。我能想到的几种情况我都认为可以接受。
aschepler

1
@achelper如果您要为没有FPU的设备编程,那么牺牲精度和程序员时间来将算法从浮点转换为整数(具有适当的比例因子)可能是值得的。
plugwash

Answers:


123

浮点数与整数:

从历史上看,浮点运算可能比整数运算要慢得多。在现代计算机上,情况已不再是如此(在某些平台上,速度稍慢一些,但是除非您编写完美的代码并针对每个周期进行优化,否则差异将被代码中的其他效率低下所淹没)。

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

非常有限的处理器(便宜的手机和烤面包机)上,通常没有浮点硬件,因此浮点操作需要在软件中进行仿真。这很慢-比整数算术要慢几个数量级。

就像我说过的那样,人们期望他们的电话和其他设备的行为越来越像“真实的计算机”,并且硬件设计人员正在迅速增强FPU以满足这种需求。除非您要追踪每个上一个周期,否则除非您为很少或没有浮点支持的非常有限的CPU编写代码,否则性能差异对您而言并不重要。

不同大小的整数类型:

通常,CPU最快可以以其本机字大小的整数进行操作(有些警告是关于64位系统的)。32位操作通常比现代CPU上的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(如计算机中使用的CPU)上,高需求,高复杂度的操作将很快,因为用户愿意为此付费。您可能不愿意为烤面包机多花3美元来获得快速的FP倍增,因此,廉价的CPU将跳过这些指令。

在几乎所有处理器上,低需求,高复杂度的操作通常会变慢;只是没有足够的好处来证明成本。

如果有人去考虑它们,低需求,低复杂度的操作将很快,否则将不存在。

进一步阅读:

  • Agner Fog维护了一个不错的网站,其中包含许多有关低级性能细节的讨论(并且具有非常科学的数据收集方法来对其进行备份)。
  • 尽管《英特尔®64和IA-32架构优化参考手册》(PDF下载链接位于页面的下方)也涵盖了许多此类问题,尽管它专注于一种特定的架构家族。

在隔离地讨论操作码时序/吞吐量时,它仍然要慢得多(对于大多数数学运算-例如,排除MOV等)。我找不到以前有用的经验

15
我喜欢您的复杂性/需求表。这确实是一个很好的总结方法。+1
jalf

@pst:仅当您考虑延迟时;吞吐量是一种更有意义的度量,并且现代的非嵌入式CPU可以(至少)在每个周期进行一个FP乘加运算。
斯蒂芬·佳能

+1非常正确-我正试图强调这一点,但是即使它不是直接的,您也做得更好。

很棒的答案!写得很好,是我在该主题上读过的最好的答案之一。甚至链接都很棒。
Mecki 2013年

15

绝对。

首先,当然,它完全取决于所讨论的CPU体系结构。

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

  • 对于简单操作,整数类型是fast。例如,整数加法通常只有一个周期的等待时间,而整数乘法通常约为2-4个周期,即IIRC。
  • 浮点类型的执行速度要慢得多。但是,在当今的CPU上,它们具有出色的吞吐量,每个浮点单元通常可以每个周期退出一个操作,从而导致与整数操作相同(或相似)的吞吐量。但是,延迟通常会更糟。浮点加法通常有大约4个周期的延迟(整数对1)。
  • 对于某些复杂的操作,情况是不同的,甚至是相反的。例如,FP的除法可能比整数除法的等待时间,这仅是因为在两种情况下都难以实现该操作,但是在FP值上它更有用,因此可能需要花费更多的精力(和晶体管)来优化这种情况。

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

免责声明:所有提到的时序和特性都只是从内存中提取的。我没有看任何东西,所以可能是错误的。;)

对于不同的整数类型,答案取决于CPU体系结构。x86架构由于其悠久的历史而不得不原生支持8、16、32(今天是64)位操作,并且总体而言,它们都同样快(它们使用基本相同的硬件,而零取出所需的高位)。

但是,在其他CPU上,小于或的数据类型int加载/存储的成本可能更高(将字节写入内存可能必须通过加载其所在的整个32位字,然后进行位屏蔽来更新)来完成。寄存器中的单个字节,然后将整个字写回)。同样,对于大于的数据类型int,某些CPU可能必须将操作分为两部分,分别分别加载/存储/计算下半部分和上半部分。

但是在x86上,答案是它几乎没有关系。由于历史原因,要求CPU对每种数据类型都具有相当强大的支持。因此,您可能会注意到的唯一区别是浮点运算具有更长的延迟(但吞吐量相似,因此,至少在您正确编写代码的情况下,它们本身并不)。


10

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

整数提升意味着与int相比,在较小的整数类型上不会发生该语言中的任何操作(加法,按位,逻辑等)。因此,对char / short / int的操作通常相当快,因为​​前者的操作被提升为后者。

在整数提升之外,还有“通常的算术转换”,这意味着C努力使两个操作数具有相同的类型,如果两个操作数不同,则将它们之一转换为较大的两个。

但是,CPU可以在8、16、32等级别上执行各种加载/存储操作。在8位和16位体系结构上,这通常意味着尽管进行了整​​数提升,但8位和16位类型却更快。在32位CPU上,实际上可能意味着较小的类型速度较慢,因为它希望将所有内容整齐地排列在32位块中。32位编译器通常会优化速度,并在比指定的更大的空间中分配较小的整数类型。

尽管通常较小的整数类型当然要比较大的整数类型占用更少的空间,所以如果要优化RAM大小,则首选它们。


1
你说的不是真的。诚然,必须根据只能讲故事一半的标准来推广整数。C有一个“假设”规则,因此,如果您编写类似uint8_t c = a + b的内容,则逻辑上将a和b提升,然后相加,然后丢弃高位,但编译器可以自由地将其实现为8位加法因为那样会产生相同的结果。
plugwash

1
@plugwash只有允许编译器可以确保升级的副作用仍然存在时,才可以进行优化。因此,如果有uint8_t b=255;,然后执行,uint8_t a = (b + 1)/256;则结果必须为1而不是0。如果有,uint8_t b; b << 15则编译器必须在int16位的情况下调用未定义的行为。等等。
伦丁

3
@Lundin说“编译器必须调用未定义的行为”是什么意思?编译器没有义务对具有未定义行为的代码进行调用或执行任何操作:)
Jonathan Wakely,

3
@Lundin根本不是真的。如果副作用是由不确定的行为引起的,那么经常会被精确地消除掉。如果您以为遇到意外事故时总会崩溃,那真是令人意外。未定义的行为意味着任何事情都可能发生。
Jonathan Wakely

3
@Lundin不,那真的不安全。这不是现代编译器的工作方式。检测是否发生溢出可能取决于优化级别,是否内联函数,调用函数的上下文等。等等。涉及许多变量,并且同一编译器每次都将做同样的事情并不是真的。
乔纳森·威克里

7

上面的第一个答案很好,我将其中的一小部分复制到了以下重复的副本中(因为这是我最先结束的地方)。

“ 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位整数:444us
将5添加到1100100 8位整数:358us

初始化并设置1100100 16位整数:666us
加5到1100100 16位整数:359us

初始化并设置1100100 32位整数:870us
将5添加到1100100 32位整数:276us

初始化并设置1100100 64位整数:2201us
将5添加到1100100 64位整数:659us


2

积分算术和浮点算术之间是否有性能差异?

是。但是,这很大程度上取决于平台和CPU。不同的平台可以以不同的速度执行不同的算术运算。

话虽如此,有关的答复更为具体。 pow()是适用于双精度值的通用例程。通过为它提供整数值,它仍然可以完成处理非整数指数所需的所有工作。使用直接乘法会绕开很多复杂性,而这正是速度发挥作用的地方。实际上,这实际上不是不同类型的问题,而是要绕过使用任何指数使pow函数所需的大量复杂代码。


还请答复哪个更快,为什么?...速度是不同的,因为它们的表示形式是不同的。因此,更有趣的事情是知道为什么
纳瓦兹

@Nawaz:这确实取决于平台。与您的体系结构的寄存器大小和数量有很多关系(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所需的普通乘法复杂。
大卫·罗德里格斯(DavidRodríguez)-dribeas 2011年

@David:这是一个很好的评论。我认为您应该发布一个详细的答案,从逻辑电路级别到sqrt都对此进行解释!
纳瓦兹

2
@Nawaz:那么您需要的是一本书。SO并不真正适合于新颖的答案。
jalf

1

通常,整数数学比浮点数学快。这是因为整数数学涉及更简单的计算。但是,在大多数操作中,我们谈论的时钟少于十二个。不是毫,微米,纳米或壁虱;时钟。在现代内核中,每秒发生2-3亿次之间的事件。另外,由于486的许多内核都具有一组浮点处理单元(FPU),这些单元硬连线以有效地执行浮点运算,并且通常与CPU并行。

结果,尽管从技术上讲它比较慢,但浮点计算仍然如此之快,以至于任何试图对差值进行计时的尝试都会在计时机制和线程调度中固有比执行计算所需的更多错误。在可能的情况下使用int,但在不能的情况下使用int,不必过多担心相对计算速度。


1
-1不正确:“在大多数操作中,我们所谈论的少于十二个时钟。” 大多数现代的x86 CPU可以在1-2个周期(整数和浮点)中进行算术运算。“因为486,许多内核都有一个... FPU”-实际上,自奔腾以来,所有x86 CPU都支持FP硬件。
sleske 2013年

1

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

具有浮点协处理器的平台可能比积分算法要慢,这是因为必须在协处理器之间来回传输值。

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

如果浮点计算是通过软件仿真的,则积分算法将更快。

如有疑问,请简介。

在优化之前,请使程序正确且稳定地工作。


0

不,不是。这当然取决于CPU和编译器,但是性能差异通常可以忽略不计-即使有的话。


3
视情况而定。在日常应用程序代码中,它通常可以忽略不计。但是在高性能数字代码中,它可以发挥很大的作用。我可以命名至少一个CPU,其double加法实际上比int加法慢14倍,这在FP繁重的应用程序中绝对可以感觉到;)
jalf

0

浮点数和整数算术之间肯定有区别。根据CPU的特定硬件和微指令,您可以获得不同的性能和/或精度。良好的Google术语,用于精确的描述(我也不知道):

FPU x87 MMX SSE

关于整数的大小,最好使用平台/体系结构字的大小(或两倍),具体int32_t取决于x86和int64_tx86_64上的大小。SOme处理器可能具有可同时处理其中几个值的固有指令(例如SSE(浮点数)和MMX),这将加快并行加法或乘法运算的速度。

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.