如果有或没有x32 ABI,如何为x86_64定义 int_fastN_t类型?
除其他外, x32 ABI为x86_64体系结构生成的代码指定了32位指针。 它结合了x86_64架构(包括64位CPU寄存器)的优点和减少的32位指针开销。
头定义了
int_fast8_t
, int_fast16_t
, int_fast32_t
和int_fast64_t
(以及相应的无符号类型uint_fast8_t
等),每个都是:
一种整数类型,通常在具有至少指定宽度的所有整数类型中运行最快
用脚注:
指定类型不能保证最快用于所有目的; 如果实现没有明确的理由选择一种类型而不是另一种类型,它将简单地选择一些满足符号和宽度要求的整数类型。
(引自N1570 C11草案 。)
问题是,无论是否有x32 ABI,如何为x86_64架构定义[u]int_fast16_t
和[u]int_fast32_t
类型? 是否有指定这些类型的x32文档? 它们是否应与32位x86定义(均为32位)兼容,或者,由于x32可以访问64位CPU寄存器,它们是否应该具有相同的大小,有或没有x32 ABI? (请注意,无论x32 ABI是否正在使用,x86_64都有64位寄存器。)
这是一个测试程序(取决于特定于gcc的__x86_64__
宏):
#include #include #include int main(void) { #if defined __x86_64__ && SIZE_MAX == 0xFFFFFFFF puts("This is x86_64 with the x32 ABI"); #elif defined __x86_64__ && SIZE_MAX > 0xFFFFFFFF puts("This is x86_64 without the x32 ABI"); #else puts("This is not x86_64"); #endif printf("uint_fast8_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast8_t)); printf("uint_fast16_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast16_t)); printf("uint_fast32_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast32_t)); printf("uint_fast64_t is %2zu bits\n", CHAR_BIT * sizeof (uint_fast64_t)); }
当我用gcc -m64
编译它时,输出是:
This is x86_64 without the x32 ABI uint_fast8_t is 8 bits uint_fast16_t is 64 bits uint_fast32_t is 64 bits uint_fast64_t is 64 bits
当我用gcc -mx32
编译它时,输出是:
This is x86_64 with the x32 ABI uint_fast8_t is 8 bits uint_fast16_t is 32 bits uint_fast32_t is 32 bits uint_fast64_t is 64 bits
(除了第一行之外,它将输出与gcc -m32
匹配,后者生成32位x86代码)。
这是glibc中的一个错误(定义了标题),还是遵循了一些x32 ABI要求? x32 ABI文档或x86_64 ABI文档中没有对
[u]int_fastN_t
类型的引用,但可能还有其他内容指定它。
有人可能认为fast16和fast32类型应该是64位或x32,因为64位寄存器是可用的; 会更有意义的是当前的行为吗?
(我已经基本上编辑了原始问题,它只询问了x32 ABI。现在问题是x86_64是否有x32。)
我编译了以下示例代码来检查生成的代码,以获得具有不同整数类型的简单求和:
#include typedef int16_t INT; //typedef int32_t INT; //typedef int64_t INT; INT foo() { volatile INT a = 1, b = 2; return a + b; }
然后我反汇编了每个整数类型生成的代码。 编译命令是gcc -Ofast -mx32 -c test.c
请注意,在完整的64位模式下,生成的代码几乎相同,因为我的代码中没有指针(只有%rsp
而不是%esp
)。
使用int16_t
它会发出:
00000000 : 0: b8 01 00 00 00 mov $0x1,%eax 5: ba 02 00 00 00 mov $0x2,%edx a: 67 66 89 44 24 fc mov %ax,-0x4(%esp) 10: 67 66 89 54 24 fe mov %dx,-0x2(%esp) 16: 67 0f b7 54 24 fc movzwl -0x4(%esp),%edx 1c: 67 0f b7 44 24 fe movzwl -0x2(%esp),%eax 22: 01 d0 add %edx,%eax 24: c3 retq
使用int32_t
:
00000000 : 0: 67 c7 44 24 f8 01 00 00 00 movl $0x1,-0x8(%esp) 9: 67 c7 44 24 fc 02 00 00 00 movl $0x2,-0x4(%esp) 12: 67 8b 54 24 f8 mov -0x8(%esp),%edx 17: 67 8b 44 24 fc mov -0x4(%esp),%eax 1c: 01 d0 add %edx,%eax 1e: c3 retq
并使用int64_t
:
00000000 : 0: 67 48 c7 44 24 f0 01 00 00 00 movq $0x1,-0x10(%esp) a: 67 48 c7 44 24 f8 02 00 00 00 movq $0x2,-0x8(%esp) 14: 67 48 8b 54 24 f0 mov -0x10(%esp),%rdx 1a: 67 48 8b 44 24 f8 mov -0x8(%esp),%rax 20: 48 01 d0 add %rdx,%rax 23: c3 retq
现在,我并没有声称确切地知道编译器为什么生成了这个代码(也许volatile
关键字与非寄存器大小的整数类型组合不是最好的选择?)。 但是根据生成的代码,我们可以得出以下结论:
- 最慢的类型是
int16_t
。 它需要额外的指令来移动值。 - 最快的类型是
int32_t
。 虽然32位和64位版本具有相同数量的指令,但32位代码的字节数较短,因此它将更加缓存,因此速度更快。
因此,快速类型的自然选择是:
- 对于
int_fast16_t
,选择int32_t
。 - 对于
int_fast32_t
,选择int32_t
。 - 对于
int_fast64_t
,选择int64_t
(还有什么)。
一般来说,你会期望32位整数类型比x86-64 CPU上的64位整数类型略快。 部分是因为它们使用较少的内存,但也因为64位指令需要在其32位对应物上额外添加前缀字节。 32位除法指令明显快于64位指令,但其他指令执行延迟相同。
将它们加载到64位寄存器时通常不需要扩展32位。 虽然在这种情况下CPU会自动对值进行零扩展,但这通常只是一个好处,因为它可以避免部分寄存器停顿。 加载到寄存器上部的内容不如整个寄存器被修改的事实重要。 寄存器上半部分的内容无关紧要,因为当它们用于保存32位类型时,它们通常仅用于32位指令,这些指令仅适用于寄存器的低32位部分。
当使用x32和x86-64 ABI时, int_fast32_t
类型的大小之间的不一致可能是最好的,因为指针是64位宽。 每当向指针添加32位整数时,都需要进行扩展,这在使用x86-64 ABI时更容易发生。
另一个需要考虑的因素是x32 ABI的重点是通过使用较小的类型来获得更好的性能。 任何受益于指针和相关类型较小的应用程序也应该受益于int_fast32_t
小一些。
强硬。 我们来看看int_fast8_t。 如果开发人员使用大型数组来存储大量8位有符号整数,那么由于缓存,int8_t将是最快的。 我声明使用大型int_fast8_t数组可能是一个坏主意。
您需要使用大型代码库,并且如果使用int_fast8_t签名,则系统地替换int8_t和signed char以及plain char。 然后使用int_fast8_t的不同typedef对代码进行基准测试,并测量最快的代码。
请注意,未定义的行为将发生变化。 例如,如果类型为int8_t,则赋值255将给出-1的结果,否则为255。