看到Alexandre C在另一个话题中的reply,我很好奇是否内置类型有性能差异:
char
vsshort
vsint
vs.float
vs.double
。
通常我们不考虑这样的性能差异(如果有的话)在我们的实际项目中,但出于教育目的,我想知道这一点。可以提出一般性问题:
整数算术和浮点算术之间是否存在性能差异?
哪个更快?为什么更快?请解释一下。
看到Alexandre C在另一个话题中的reply,我很好奇是否内置类型有性能差异:
char
vsshort
vsint
vs.float
vs.double
。
通常我们不考虑这样的性能差异(如果有的话)在我们的实际项目中,但出于教育目的,我想知道这一点。可以提出一般性问题:
整数算术和浮点算术之间是否存在性能差异?
哪个更快?为什么更快?请解释一下。
浮点数与整数:
历史上,浮点运算可能比整数运算要慢得多。但在现代计算机上,情况已经不再是这样了(在某些平台上它可能略慢一些,但除非您编写完美的代码并针对每个周期进行优化,否则差异将被代码中的其他低效性所掩盖)。
在一些性能有限的处理器上,例如高端手机,浮点运算可能比整数运算稍微慢一些,但通常仅相差一个数量级(或更好),只要硬件支持浮点运算。值得注意的是,随着手机需要运行越来越多的通用计算工作负载,这种差距正在迅速缩小。
在极度性能有限的处理器上(便宜的手机和你的烤面包机),通常没有浮点硬件,因此需要在软件中模拟浮点运算。这很慢——比整数运算慢几个数量级。
但如我所说,人们希望他们的手机和其他设备表现得越来越像“真正的计算机”,而硬件设计师正在迅速加强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会省略掉这些指令。
低需求、高复杂度的操作通常在几乎所有处理器上都很慢;没有足够的好处来证明它们的成本。
低需求、低复杂度的操作如果有人费心思去考虑,就会很快,否则就不存在。
进一步阅读:
当然可以。
首先,这完全取决于所涉及的CPU架构。
然而,整数和浮点类型的处理方式非常不同,因此以下情况几乎总是成立:
在某些CPU上,双精度比浮点速度慢得多。在某些架构中,没有专用的双精度硬件,因此它们通过传递两个浮点大小的块来处理,从而导致您的吞吐量更差,并且延迟增加了一倍。在其他情况下(例如x86 FPU),两种类型都转换为相同的内部格式(80位浮点格式,在x86的情况下),因此性能相同。在其他情况下,浮点和双精度都有适当的硬件支持,但是由于浮点具有较少的位数,所以可以稍微快一点,通常会减少相对于双精度操作的延迟。
免责声明:所有提到的定时和特征都只是记忆中提取的。我没有查找任何信息,因此可能错误。:)
对于不同的整数类型,答案会因为CPU架构的不同而有很大差异。由于x86架构历史悠久且曲折,必须原生支持8、16、32位(现在还有64位)操作,并且它们基本上都一样快(它们使用的硬件基本相同,只需根据需要清零高位即可)。
然而,在其他CPU上,比int
小的数据类型可能更加昂贵,需要更多的时间来加载/存储(将一个字节写入内存可能必须通过加载其所在的整个32位字并进行位掩码操作来更新寄存器中的单个字节,然后将整个字写回)。同样地,对于比int
大的数据类型,某些CPU可能必须将操作分成两部分,分别加载/存储/计算低半部分和高半部分。
但是在x86上,答案大多数情况下并不重要。由于历史原因,CPU需要对每种数据类型都提供强大的支持。因此,您可能注意到的唯一区别是浮点运算具有更高的延迟(但吞吐量类似,因此如果编写正确的代码,它们并不会慢)。
我认为没有人提到整数提升规则。在标准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大小,它们是更好的选择。
上面的第一个答案非常好,我将其中一小块复制到了下面的重复内容(因为这是我最先看到的地方)。
我想提供以下代码,对各种整数大小进行分配、初始化和执行一些算术运算进行性能分析:
#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-30亿次的时钟周期。此外,自486以来,许多核心都有一组浮点处理单元或FPU,这些单元被硬连线以高效地执行浮点运算,并且通常与CPU并行。
由于这些原因,尽管从技术上讲它更慢,但浮点计算仍然非常快,任何试图计算差异的尝试都会在计时机制和线程调度中具有更多的误差,而不是实际执行计算所需的时间。在可以使用整数时,请使用整数,但要了解何时不能使用整数,并不要过于担心相对计算速度。
这取决于处理器和平台的组成。
具有浮点协处理器的平台可能比整数算术慢,因为值必须在协处理器之间传输。
如果浮点处理在处理器核心内部进行,则执行时间可能可以忽略不计。
如果浮点计算由软件模拟,则整数算术将更快。
如果不确定,进行分析。
在优化之前,请确保编程正确且稳健。
不,其实并没有。当然这取决于CPU和编译器,但性能差异通常是可以忽略的,如果有的话。
浮点数和整数算法之间的差异是肯定存在的。根据CPU的特定硬件和微指令,性能和/或精度也会有所不同。对于准确的描述,可以使用以下谷歌术语(我也不太确定):
FPU x87 MMX SSE
关于整数的大小,最好使用平台/架构的字长(或双倍),在x86上为int32_t
,在x86_64上为int64_t
。一些处理器可能具有处理多个这些值并行相加或相乘的内部指令(例如SSE(浮点数)和MMX),这将加快运算速度。