(我指的是Intel CPU,主要是GCC,但可能是ICC或MSVC)
使用int8_t
是否属实,int16_t
或int64_t
与int32_t
相比效率较低,原因是生成的额外指令用于在CPU字大小和所选内容之间进行转换可变大小?
如果有人有任何示例或最佳做法,我会感兴趣吗?我有时使用较小的可变大小来减少缓存行负载,但是我说只消耗了50个字节的缓存行,其中一个变量是8位int,通过使用剩余的缓存行空间并将8位int提升为a,可以更快地处理32位int等?
答案 0 :(得分:5)
您可以将更多uint8_t
填充到缓存行中,因此加载N uint8_t
将比加载N uint32_t
更快。
此外,如果您使用带有SIMD指令的现代英特尔芯片,智能编译器将对其进行矢量化。同样,在代码中使用一个小变量将允许编译器将更多通道填充到SIMD寄存器中。
我认为最好使用最小的尺寸,并将细节留给编译器。当涉及到这样的东西时,编译器可能比你(和我)更聪明。对于许多操作(比如无符号加法),编译器可以对uint8
,uint16
或uint32
使用相同的代码(并忽略高位),因此没有速度差异。
最重要的是,缓存未命中比任何算术或逻辑操作都要昂贵,所以担心缓存(以及数据大小)几乎总是比简单算术更好。
(过去很长一段时间以来,在Sun工作站上使用double
明显快于float
,因为硬件仅支持double
。我不认为对于现代x86来说更是如此,因为SIMD硬件(SSE等)直接支持单精度和双精度。)
答案 1 :(得分:4)
Mark Lakata在正确的方向上回答了问题。
我想补充一些观点。
理解和做出优化决策的绝佳资源是Agner文档。
指令表文档具有最常见指令的延迟。您可以看到其中一些在原生尺寸版本中表现更好
例如,可以删除mov
,mul
具有较少的延迟
然而,我们在谈论获得1个时钟,我们必须执行大量指令来补偿缓存未命中。
如果这是整个故事,那就不值得了。
解码器带来了真正的问题。
当你使用一些长度变化的前缀(并且你将使用非原生大小的单词)时,解码器需要额外的周期。
操作数大小前缀因此改变了指令其余部分的长度。预编码器无法在单个时钟周期内解决此问题。从此错误中恢复需要6个时钟周期。因此,避免这种长度变化的前缀非常重要。
现在,不再是最近(但现在仍然存在)的微观,惩罚是严厉的,特别是有一些类型的算术指示。
在后来的微型游戏中,这已被减轻,但惩罚仍然存在。
要考虑的另一个方面是使用非本机大小需要在指令前加上,从而生成更大的代码。
这是尽可能接近语句“生成的附加指令,用于在CPU字大小和所选变量大小之间进行转换”,因为Intel CPU可以处理非本机字大小。
对于其他特别是RISC,CPU,这通常不正确,可以生成更多指令。
因此,当您正在充分利用数据缓存时,您也错误地使用了指令缓存。
在常见的x64 ABI上,堆栈必须在16字节边界上对齐,并且通常编译器会将本地字大小的本地变量保存为近似值(例如64位系统上的DWORD),这也是值得的。
只有在分配足够数量的本地变量或者使用数组或打包结构时,才能从使用小变量大小中获益。
如果你声明一个uint16_t
var,它可能需要一个uint64_t
的相同堆栈空间,所以最好选择最快的大小。
此外,当它来到数据缓存时,重要的是locality,而不仅仅是数据大小。
幸运的是,您无需在拥有小数据或小代码之间做出决定。
如果您有大量数据,通常使用数组或指针以及使用中间变量来处理。这一行代码就是一个例子。
t = my_big_data[i];
我的方法是:
保持数据的外部表示,即my_big_data
数组,尽可能小。例如,如果该数组存储温度对每个元素使用编码uint8_t
。
保持数据的内部表示,即t
变量,尽可能接近CPU字大小。例如,t
可以是uint32_t
或uint64_t
。
通过这种方式,您可以优化两个缓存并使用原生单词大小
作为奖励,您可以稍后决定切换到SIMD指令,而无需重新打包my_big_data
内存布局。
真正的问题是程序员花了太多时间在错误的地方和错误的时间担心效率;过早优化是编程中所有邪恶(或至少大部分)的根源 d。 Knuth的
当您设计结构时,内存布局会受到问题驱动。例如,年龄值需要8位,城市距离以英里为单位需要16位
在对算法进行编码时,使用编译器已知的最快类型来获得该范围。例如,整数比浮点数快,uint_fast8_t
不比uint8_t
慢。
当时需要通过更改算法(使用更快的类型,消除冗余操作等)来改善性能开始,然后如果需要数据结构(通过对齐) ,填料,包装等)。