三位C/C++程序员走进了一家酒吧。第一位争辩说 sizeof(void*)
等于 sizeof(long)
,第二位争辩说 sizeof(void*)
等于 sizeof(int)
,第三位争辩说是 sizeof(long long)
。与此同时,他们每个人都对,也都错(需要上关于可移植C代码的课)。这是怎么回事呢?
程序员在写“Hello, World!”后,可能写的第一个程序是这样的:
#include <stdio.h> int main () { printf("sizeof(int): %zu\n", sizeof(int)); printf("sizeof(long): %zu\n", sizeof(long)); printf("sizeof(long long): %zu\n", sizeof(long long)); printf("sizeof(void*): %zu\n", sizeof(void*)); }
注意使用了 %zu
格式说明符,这是C99新增的特性,但对于老编译器不一定兼容!(这篇文章更多是讨论将旧代码迁移到新机器时的考虑,而不是将新代码迁移到旧机器。没有标准兼容的C编译器会使得编写可移植的C代码更加困难,这也是另一个博客话题)。
当我在我的x86-64 OSX机器上运行这段代码时,得到的输出是:
sizeof(int): 4 sizeof(long): 8 sizeof(long long): 8 sizeof(void*): 8
看起来我就是故事中第一位程序员的情况,因为在我的机器上,sizeof(long)
等于sizeof(void*)
。还要注意,sizeof(long long)
也是相等的。
但如果我在一台32位机器上编译这段代码,会发生什么呢?幸运的是,我的处理器支持32位二进制的向后兼容,所以我可以在本地交叉编译并运行。示例:
➜ clang sizeof.c -Wall -Wextra -Wpedantic ➜ file a.out a.out: Mach-O 64-bit executable x86_64 ➜ clang sizeof.c -Wall -Wextra -Wpedantic -m32 ➜ file a.out a.out: Mach-O executable i386 ➜ ./a.out sizeof(int): 4 sizeof(long): 4 sizeof(long long): 8 sizeof(void*): 4
哇,突然间 sizeof(void*)
等于 sizeof(int)
等于 sizeof(long)
!这似乎是故事中第二位程序员的情况。
第一位和第二位程序员可能都同意指针的大小等于各自机器的字长,但这个假设对于可移植的C/C++代码来说也是错误的!
第三位程序员经历了安装Windows编译器的痛苦过程,并构建了一个64位命令行应用(公平地说,安装OSX的命令行工具更糟糕;安装大多数操作系统的编译器都很麻烦)。当他们运行该程序时,看到的是:
sizeof(int): 4 sizeof(long): 4 sizeof(long long): 8 sizeof(void*): 8
这是第三种情况(故事中的第三位程序员)。在这种情况下,只有 sizeof(long long)
等于 sizeof(void*)
数据模型
这些程序员看到的情况被称为数据模型。
第一位程序员在一台64位的x86-64 OSX机器上运行,使用的是LP64数据模型,其中long(L),long long(更大的long long),以及指针(P)是64位的,但int是32位的。
第二位程序员在32位的x86 OSX机器上运行,使用的是ILP32数据模型,其中int(I),long(L),和指针(P)是32位的,但long long是64位的。
第三位程序员在64位的x86-64 Windows机器上运行,使用的是LLP64数据模型,其中只有long long(LL)和指针(P)是64位的,int和long是32位的。
数据模型 | sizeof(int) | sizeof(long) | sizeof(long long) | sizeof(void*) | 示例 |
---|---|---|---|---|---|
ILP32 | 32b | 32b | 64b | 32b | Win32, i386 OSX & Linux |
LP64 | 32b | 64b | 64b | 64b | x86-64 OSX & Linux |
LLP64 | 32b | 32b | 64b | 64b | Win64 |
还有一些更旧的数据模型,比如LP32(Windows 3.1,Macintosh,其中int是16位),以及一些更奇特的数据模型,如ILP64和SILP64。因此,了解数据模型对于编写可移植的C/C++代码非常重要。
历史背景
计算机中,地址空间不足的问题一直存在,并且会继续存在。随着计算机性能和内存的便宜,应用程序变得越来越大。公司希望销售具有更大字长的芯片来访问更多内存,但早期的采用者不想购买无法运行其最喜爱应用程序的计算机(因为应用程序还没有为新硬件编译)。有人大喊虚拟机,接着一个椅子飞过来。
本文强调了为什么LP64优于ILP64的原因:ILP64使得只需要32位精度的可移植C代码更难维护(在ILP64中,int是64位,而short是16位!)。它提到,如果数据结构不包含指针,它们的大小在LLP64和ILP32之间是相同的,这是微软的方向。LLP64本质上是具有64位指针的ILP32模型。
Linux还支持一个叫做x32的ABI,它可以使用x86-64 ISA的改进,但使用32位指针来减小数据结构的大小,避免使用64位指针。
如果想了解更多关于字长和数据模型的演变,以及由此产生的“苦难”,这篇论文是一个很好的参考。它描述了微软最终放弃对16位数据模型的支持,改为支持Windows XP 64位。它提到,业界在LP64、LLP64和ILP64之间分歧较大,因为从ILP32向移植的代码会以不同的方式出错。它还指出,Windows应用程序中使用long的情况更多,而Unix应用程序中使用int的情况更多。它还提出了一个关键点,很多来自ILP32时代的程序员假设 sizeof(int) == sizeof(long) == sizeof(void*)
,但在LP64/LLP64时代这并不成立。
论文还指出,C语言中的typedef直到1977年才加入,那时硬件制造商仍然无法达成一致,无法确定char(CHAR_BITS)有多少位,而有些机器使用的是24位寻址方案。stdint.h和inttypes.h还没有出现。
这篇文章讨论了从ILP32到LP64切换的两个主要类别的影响,并提供了非常好的代码示例。文章最后的那一节值得一读,提供了代码审查时要关注的要点。
结论
字长或ISA并不能告诉你 sizeof(int)
、sizeof(long)
或 sizeof(long long)
的具体大小。我们还看到,一台机器可以支持多种不同的数据模型(当我用 -m32
标志编译并运行相同的代码时)。
C标准告诉你这些类型的最小保证大小,但数据模型(作为ABI的一部分,虽然外部但遵循C标准)则告诉你标准整数和指针的具体大小。
原文地址:Data Models and Word Size
补充内容
更多资料:
- 64-Bit Programming Models: Why LP64?
- The Long Road to 64 Bits
- The UNIX System – 64bit and Data Size Neutrality
- 64-bit data models
- C Language Data Type Models: LP64 and ILP32
- ILP64, LP64, LLP64
- x32 ABI
- difference between stdint.h and inttypes.h
- Abstract Data Models
- The New Data Types
- Is there any reason not to use fixed width integer types (e.g. uint8_t)?
- Why did the Win64 team choose the LLP64 model?