【发布时间】:2023-03-16 04:56:01
【问题描述】:
有一个演讲,CppCon 2016: Chandler Carruth “Garbage In, Garbage Out: Arguing about Undefined Behavior...",Carruth 先生展示了 bzip 代码中的一个示例。他们使用uint32_t i1 作为索引。在 64 位系统上,阵列访问 block[i1] 将执行 *(block + i1)。问题是 block 是一个 64 位指针,而 i1 是一个 32 位数字。加法可能会溢出,并且由于无符号整数已经定义了溢出行为,编译器需要添加额外的指令以确保即使在 64 位系统上也确实可以实现这一点。
我还想用一个简单的例子来说明这一点。所以我用各种有符号和无符号整数尝试了++i 代码。以下是我的测试代码:
#include <cstdint>
void test_int8() { int8_t i = 0; ++i; }
void test_uint8() { uint8_t i = 0; ++i; }
void test_int16() { int16_t i = 0; ++i; }
void test_uint16() { uint16_t i = 0; ++i; }
void test_int32() { int32_t i = 0; ++i; }
void test_uint32() { uint32_t i = 0; ++i; }
void test_int64() { int64_t i = 0; ++i; }
void test_uint64() { uint64_t i = 0; ++i; }
有了g++ -c test.cpp 和objdump -d test.o,我得到了像
这个:
000000000000004e <_Z10test_int32v>:
4e: 55 push %rbp
4f: 48 89 e5 mov %rsp,%rbp
52: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
59: 83 45 fc 01 addl $0x1,-0x4(%rbp)
5d: 90 nop
5e: 5d pop %rbp
5f: c3 retq
说实话:我对 x86 汇编的了解相当有限,所以我的以下 结论和问题可能很幼稚。
前两条指令似乎只是来自函数的调用, 最后三个似乎是返回值。仅删除这些行, 以下内核用于各种数据类型:
-
int8_t:4: c6 45 ff 00 movb $0x0,-0x1(%rbp) 8: 0f b6 45 ff movzbl -0x1(%rbp),%eax c: 83 c0 01 add $0x1,%eax f: 88 45 ff mov %al,-0x1(%rbp) -
uint8_t:19: c6 45 ff 00 movb $0x0,-0x1(%rbp) 1d: 80 45 ff 01 addb $0x1,-0x1(%rbp) -
int16_t:28: 66 c7 45 fe 00 00 movw $0x0,-0x2(%rbp) 2e: 0f b7 45 fe movzwl -0x2(%rbp),%eax 32: 83 c0 01 add $0x1,%eax 35: 66 89 45 fe mov %ax,-0x2(%rbp) -
uint16_t:40: 66 c7 45 fe 00 00 movw $0x0,-0x2(%rbp) 46: 66 83 45 fe 01 addw $0x1,-0x2(%rbp) -
int32_t:52: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 59: 83 45 fc 01 addl $0x1,-0x4(%rbp) -
uint32_t:64: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 6b: 83 45 fc 01 addl $0x1,-0x4(%rbp) -
int64_t:76: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp) 7d: 00 7e: 48 83 45 f8 01 addq $0x1,-0x8(%rbp) -
uint64_t:8a: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp) 91: 00 92: 48 83 45 f8 01 addq $0x1,-0x8(%rbp)
比较签名和未签名的版本,我期望先生。 Carruth 谈到会生成额外的屏蔽指令。
但是对于int8_t,我们将一个字节(movb)加载到%rbp,然后加载并补零
将长 (movzbl) 放入累加器 %eax。添加(add)是
由于未定义溢出,因此在没有任何大小规范的情况下执行
反正。无符号版本直接使用字节指令。
add 和 addb/addw/addl/addq 都取相同数量的
周期(延迟),因为 Intel Sandy Bridge CPU 具有适用于所有的硬件加法器
大小或 32 位单元在内部进行屏蔽,因此具有更长的
延迟。
我寻找了一个有延迟的表并找到了one by
agner.org。那里为
每个 CPU(在这里使用 Sandy Bridge)只有一个 ADD 条目,但我有
看不到其他宽度变体的条目。 Intel 64 and IA-32 Architectures Optimization Reference Manual 似乎也只列出了一条 add 指令。
这是否意味着在 x86 上,非本地长度整数的 ++i 实际上是
无符号类型更快,因为指令更少?
【问题讨论】:
-
需要注意的是,这些函数没有副作用并且不返回任何内容,因此编译器可以轻松将其优化为空(gcc does at
-Og)。您可能应该将整数作为参数并返回结果。 -
您是在没有优化的情况下进行构建吗?如果启用它,结果会有所不同吗?性能测量和基准测试应始终在优化的代码上进行。
-
对于此类测试,重要的是至少使用 -O2 优化级别以及清楚 -march、-mtune 开关
-
在 x86 或 ARM 等架构上计算指令是没有意义的。更多的指令可能会导致更快的执行。而是分析您的代码如果您有性能问题并优化热点。
-
第一段不是对钱德勒解释的准确总结,所以这个问题似乎是基于一个误解。
标签: c++ assembly optimization x86-64 compiler-optimization