如果有或没有x32 ABI,如何为x86_64定义 int_fastN_t类型?

除其他外, x32 ABI为x86_64体系结构生成的代码指定了32位指针。 它结合了x86_64架构(包括64位CPU寄存器)的优点和减少的32位指针开销。

头定义了int_fast8_tint_fast16_tint_fast32_tint_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关键字与非寄存器大小的整数类型组合不是最好的选择?)。 但是根据生成的代码,我们可以得出以下结论:

  1. 最慢的类型是int16_t 。 它需要额外的指令来移动值。
  2. 最快的类型是int32_t 。 虽然32位和64位版本具有相同数量的指令,但32位代码的字节数较短,因此它将更加缓存,因此速度更快。

因此,快速类型的自然选择是:

  1. 对于int_fast16_t ,选择int32_t
  2. 对于int_fast32_t ,选择int32_t
  3. 对于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。