【问题标题】:Does int32_t have lower latency than int8_t, int16_t and int64_t?int32_t 的延迟是否低于 int8_t、int16_t 和 int64_t?
【发布时间】:2015-09-22 06:30:37
【问题描述】:

(我指的是 Intel CPU,主要是 GCC,但也有 ICC 或 MSVC)

使用int8_t、int16_t 或int64_t 是否确实比int32_t 效率更低,因为生成了额外的指令来在CPU 字长和所选变量大小之间进行转换?

如果有人对此有任何示例或最佳实践,我会感兴趣吗?我有时使用较小的变量大小来减少缓存线负载,但假设我只消耗了 50 个字节的缓存线,其中一个变量是 8 位 int,通过使用剩余的缓存线空间并将 8 位 int 提升为32位整数等?

【问题讨论】:

  • 对其进行分析,然后发布您的发现。
  • 在较旧的处理器上,它确实有助于使用本机字长,但在现代处理器上,数据是为了缓存目的而批量获取的。
  • @KerrekSB 抖动会隐藏任何差异。但是,我仍然想知道是否有任何场景会生成更多指令。
  • 你熟悉 cstdint 中的全套类型吗?例如int_fast8_t 等? (en.cppreference.com/w/cpp/types/integer)
  • uint64_t:在 32 位 CPU 上,使用 64 位数据类型在任何情况下都会比 32 位数据类型慢,因为程序需要两倍的机器指令数..

标签: c++ performance assembly optimization cpu-architecture


【解决方案1】:

您可以将更多 uint8_ts 塞入缓存行,因此加载 N 个 uint8_ts 将比加载 N 个 uint32_ts 更快。

此外,如果您使用带有 SIMD 指令的现代 Intel 芯片,智能编译器会将它所能做的向量化。同样,在代码中使用一个小变量将允许编译器将更多通道填充到 SIMD 寄存器中。

我认为最好使用最小的尺寸,并将细节留给编译器。当涉及到这样的事情时,编译器可能比你(和我)更聪明。对于许多操作(比如无符号加法),编译器可以对uint8、uint16 或uint32 使用相同的代码(并且忽略高位),因此没有速度差异。

归根结底,缓存未命中比任何算术或逻辑运算都要昂贵得多,因此与简单的算术相比,担心缓存(以及数据大小)几乎总是更好。

(很长一段时间以来,在 Sun 工作站上,使用 double 明显快于 float,因为硬件只支持 double。我认为这不再是真的了现代 x86,因为 SIMD 硬件(SSE 等)直接支持单精度和双精度)。

【讨论】:

  • x64 SIMD 指令真的可以在 uint8_t 上运行吗?
  • @MikeMB 他们可以对无符号 8 位值的向量进行操作。
  • @MikeMB 看看here 并搜索 epu8 。不过,更有可能是针对有符号 8 位值的指令。
【解决方案2】:

Mark Lakata 的答案指向正确的方向。
我想补充几点。

Agner 文档是了解和做出优化决策的绝佳资源。

指令表文档对最常见的指令有延迟。您可以看到其中一些在原生尺寸版本中表现更好。
例如,mov 可能会被消除,mul 的延迟更少。
但是这里我们谈论的是获得 1 个时钟,我们必须执行大量指令来补偿缓存未命中。
如果这就是故事的全部,那就不值得了。

真正的问题在于解码器。
当您使用一些长度变化的前缀(并且您将使用非原生大小的字)时,解码器需要额外的周期。

因此,操作数大小前缀会改变指令其余部分的长度。预解码器无法在单个时钟周期内解决此问题。从这个错误中恢复需要 6 个时钟周期。因此,避免这种长度变化的前缀非常重要。

如今,在不再更新(但仍然存在)的微拱门中,惩罚是严厉的,特别是一些算术指令。
在后来的微拱中,这已得到缓解,但它仍然存在。

另一个需要考虑的方面是,使用非原生大小需要为指令添加前缀,从而生成更大的代码。 这是最接近语句“生成附加指令以在 CPU 字长和所选变量大小之间进行转换”的语句,因为 Intel CPU 可以处理非本机字长。
对于其他 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.克努特

当你设计你的结构时,内存布局是由问题驱动的。例如,年龄值需要 8 位,以英里为单位的城市距离需要 16 位。
当您对算法进行编码时,使用编译器已知在该范围内具有的最快类型。例如整数比浮点数快,uint_fast8_t 不比uint8_t 慢。

何时是时候通过更改算法(通过使用更快的类型、消除冗余操作等)开始提高性能,然后如果需要数据结构(通过对齐,填充,包装等)。

【讨论】:

  • 通常编译器会为 16b 负载生成 movzx 或 movsx(零扩展或符号扩展)。这些不需要改变长度的前缀,因此解码速度并不慢。只有当您想使用 16 位寄存器时,英特尔 CPU 上的速度才会变慢。我认为存储回 16 位内存位置也可以在没有缓慢解码指令的情况下完成。
  • 此外,在 x86-64 上,大多数操作(add、mul、cmp 等)的默认大小为 32b。它需要一个 REX 前缀来获得 64b 的操作数大小(或者如果涉及的任何寄存器是 r8 或更高)。因此,1 个额外的指令字节。所以[u]int32_t 是临时/循环计数器/等的方法,除非你需要额外的宽度。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多