編譯器假定函數(shù)返回一個(gè)int值,并截短結(jié)果指針。這行代碼在ILP32數(shù)據(jù)模型下工作正常,因?yàn)榇藭r(shí)的int和指針是同樣長度,換到LP64模型中,就不一定正確了,甚至于類型轉(zhuǎn)換都不能避免這個(gè)錯(cuò)誤,因?yàn)間etlogin()在返回之后已經(jīng)被截?cái)嗔恕?/div>
注意,scanf將向變量mylong中插入一個(gè)32位的值,而剩下的4字節(jié)就不管了。要修正這個(gè)問題,請(qǐng)?jiān)趕canf中使用%ld指定符。
這在ILP32模型下不會(huì)有任何問題,因?yàn)榇藭r(shí)的int、long都是32位,而在LP64中,當(dāng)把mylong賦值給myint時(shí),如果數(shù)值大于32位整數(shù)的最大值時(shí),數(shù)值將被截短。
編譯器發(fā)現(xiàn)的下一個(gè)錯(cuò)誤在第17行中,雖然myfunc函數(shù)只接受一個(gè)int參數(shù),但調(diào)用時(shí)卻用了一個(gè)long,參數(shù)在傳遞時(shí)會(huì)悄無聲息地被截?cái)唷?/div>
轉(zhuǎn)換截?cái)?/strong>
轉(zhuǎn)換截?cái)喟l(fā)生在把long轉(zhuǎn)換成int時(shí),比如說例1中的第19行:
myint = (int) mylong;
導(dǎo)致轉(zhuǎn)換截?cái)嗟脑蚴莍nt與long非同樣長度。這些類型的轉(zhuǎn)換通常在代碼中以如下形式出現(xiàn):
int length = (int) strlen(str);
strlen返回size_t(它在LP64中是unsigned long),當(dāng)賦值給一個(gè)int時(shí),截?cái)嗍潜厝话l(fā)生的。而通常,截?cái)嘀粫?huì)在str的長度大于2GB時(shí)才會(huì)發(fā)生,這種情況在程序中一般不會(huì)出現(xiàn)。雖然如此,也應(yīng)該盡量使用適當(dāng)?shù)亩鄳B(tài)類型(如size_t、uintptr_t等等),而不要去管它最下面的基類型是什么。
一些其他的細(xì)小問題
編譯器可捕捉到移植方面的各種問題,但不能總指望編譯器為你找出一切錯(cuò)誤。
那些以十六進(jìn)制或二進(jìn)制表示的常量,通常都是32位的。例如,無符號(hào)32位常量0xFFFFFFFF通常用來測試是否為-1:
#define INVALID_POINTER_VALUE 0xFFFFFFFF
然而,在64位系統(tǒng)中,這個(gè)值不是-1,而是4294967295;在64位系統(tǒng)中,-1正確的值應(yīng)為0xFFFFFFFFFFFFFFFF。要避免這個(gè)問題,在聲明常量時(shí),使用const,并且?guī)蟬igned或unsigned。
const signed int INVALID_POINTER_VALUE = 0xFFFFFFFF;
這行代碼將會(huì)在32位和64位系統(tǒng)上都運(yùn)行正常。
其他有關(guān)于對(duì)常量硬編碼的問題,都是基于對(duì)ILP32數(shù)據(jù)模型的不當(dāng)認(rèn)識(shí),如下:
int **p; p = (int**)malloc(4 * NO_ELEMENTS);
這行代碼假定指針的長度為4字節(jié),而這在LP64中是不正確的,此時(shí)是8字節(jié)。正確的方法應(yīng)使用sizeof():
int **p; p = (int**)malloc( sizeof(*p) * NO_ELEMENTS);
注意對(duì)sizeof()的不正確用法,例如:
sizeof(int) = = sizeof(int *);
這在LP64中是錯(cuò)誤的。
符號(hào)擴(kuò)展
要避免有符號(hào)數(shù)與無符號(hào)數(shù)的算術(shù)運(yùn)算。在把int與long數(shù)值作對(duì)比時(shí),此時(shí)產(chǎn)生的數(shù)據(jù)提升在LP64和ILP32中是有差異的。因?yàn)槭欠?hào)位擴(kuò)展,所以這個(gè)問題很難被發(fā)現(xiàn),只有保證兩端的操作數(shù)均為signed或均為unsigned,才能從根本上防止此問題的發(fā)生。
例2:
long k;
int i = -2;
unsigned int j = 1;
k = i + j;
printf("Answer: %ld\n", k);
你無法期望例2中的答案是-1,然而,當(dāng)你在LP64環(huán)境中編譯此程序時(shí),答案會(huì)是4294967295。原因在于表達(dá)式(i+j)是一個(gè)unsigned int表達(dá)式,但把它賦值給k時(shí),符號(hào)位沒有被擴(kuò)展。要解決這個(gè)問題,兩端的操作數(shù)只要均為signed或均為unsigned就可。像如下所示:
k = i + (int) j
聯(lián)合體問題(Union)
當(dāng)聯(lián)合本中混有不同長度的數(shù)據(jù)類型時(shí),可能會(huì)導(dǎo)致問題。如例3是一個(gè)常見的開源代碼包,可在ILP32卻不可在LP64環(huán)境下運(yùn)行。代碼假定長度為2的unsigned short數(shù)組,占用了與long同樣的空間,可這在LP64平臺(tái)上卻不正確。
例3:
typedef struct {
unsigned short bom;
unsigned short cnt;
union {
unsigned long bytes;
unsigned short len[2];
} size;
} _ucheader_t;
要在LP64上運(yùn)行,代碼中的unsigned long應(yīng)改為unsigned int。要在所有代碼中仔細(xì)檢查聯(lián)合體,以確認(rèn)所有的數(shù)據(jù)成員在LP64中都為同等長度。
字節(jié)序問題(Endian)
因64位平臺(tái)的差異,在移植32位程序時(shí),可能會(huì)失敗,原因可歸咎于機(jī)器上字節(jié)序的不同。Intel、IBM PC等CISC芯片使用的是Little-endian,而Apple之類的RISC芯片使用的是Big-endian;小尾字節(jié)序(Little-endian)通常會(huì)隱藏移植過程中的截?cái)郻ug。
例4:
long k;
int *ptr;
int main(void)
{
k = 2 ;
ptr = &k;
printf("k has the value %ld, value pointed to by ptr is %ld\n", k, *ptr);
return 0;
}
例4是一個(gè)有此問題的明顯例子,一個(gè)聲明指向int的指針,卻不經(jīng)意間指向了long。在ILP32上,這段代碼打印出2,因?yàn)閕nt與long長度一樣。但到了LP64上,因?yàn)閕nt與long的長度不一,而導(dǎo)致指針被截?cái)唷2还茉趺凑f,在小尾字節(jié)序的系統(tǒng)中,代碼依舊會(huì)給出k的正確答案2,但在大尾字節(jié)序(Big-endian)系統(tǒng)中,k的值卻是0。
表3說明了為什么在不同的字節(jié)序系統(tǒng)中,會(huì)因截?cái)鄦栴}而產(chǎn)生不同的答案。在小尾字節(jié)序中,被截?cái)嗟母呶坏刂分腥珵?,所以答案仍為2;而在大尾字節(jié)序中,被截?cái)嗟母呶坏刂分邪?,這樣就導(dǎo)致結(jié)果為0,所以在兩種情況下,截?cái)喽际且环Nbug。但要意識(shí)到,小尾字節(jié)序會(huì)隱藏小數(shù)值的截?cái)噱e(cuò)誤,而這個(gè)錯(cuò)誤只有在移植到大尾字節(jié)序系統(tǒng)上時(shí)才可能被發(fā)現(xiàn)。
移植到64位平臺(tái)之后的性能降低
當(dāng)代碼移植到64位平臺(tái)之后,也許發(fā)現(xiàn)性能實(shí)際上降低了。原因與在LP64中的指針長度和數(shù)據(jù)大小有關(guān),并由此引發(fā)的緩存命中率降低、數(shù)據(jù)結(jié)構(gòu)膨脹、數(shù)據(jù)對(duì)齊等問題。
由于64位環(huán)境中指針?biāo)加玫淖止?jié)更大,致使原來運(yùn)行良好的32位代碼出現(xiàn)不同程度的緩存問題,具體表現(xiàn)為執(zhí)行效率降低。可使用工具來分析緩存命中率的變化,以確認(rèn)性能降低是否由此引起。
在遷移到LP64之后,數(shù)據(jù)結(jié)構(gòu)的大小可能會(huì)改變,此時(shí)程序可能會(huì)需要更多的內(nèi)存和磁盤空間。例如,圖2中的結(jié)構(gòu)在ILP32中只需要16字節(jié),但在LP64中,卻需要32字節(jié),整整增長了100%。這緣于此時(shí)的long已是64位,編譯器為了對(duì)齊需要而加入了額外的填充數(shù)據(jù)。
通過改變結(jié)構(gòu)中數(shù)據(jù)排列的先后順序,能將此問題所帶來的影響降到最小,并能減少所需的存儲(chǔ)空間。如果把兩個(gè)32位int值放在一起,會(huì)因?yàn)樯倭颂畛鋽?shù)據(jù),存儲(chǔ)空間也隨之減少,現(xiàn)在存儲(chǔ)整個(gè)結(jié)構(gòu)只需要24字節(jié)。
在重排數(shù)據(jù)結(jié)構(gòu)之前,在根據(jù)數(shù)據(jù)使用的頻度仔細(xì)衡量,以免因降低緩存命中率而帶來性能上的損失。
如何生成64位代碼
在一些情況中,32位和64位程序在源代碼級(jí)別的接口上很難區(qū)分。不少頭文件中,都是通過一些測試宏來區(qū)分它們,不幸的是,這些特定的宏依賴于特定的平臺(tái)、特定的編譯器或特定的編譯器版本。舉例來說,GCC 3.4或之后的版本都定義了__LP64__,以便為所有的64位平臺(tái)通過選項(xiàng)-m64編譯產(chǎn)生64位代碼。然而,GCC 3.4之前的版本卻是特定于平臺(tái)和操作系統(tǒng)的。
也許你的編譯器使用了不同于__LP64__的宏,例如IBM XL的編譯器當(dāng)用-q64編譯程序時(shí),使用了__64bit__宏,而另一些平臺(tái)使用_LP64,具體情況可用__WORDSIZE來測試一下。請(qǐng)查看相關(guān)編譯器文檔,以便找出最適合的宏。例5可適用于多種平臺(tái)和編譯器:
例5:
#if defined (__LP64__) || defined (__64BIT__) || defined (_LP64) || (__WORDSIZE == 64)
printf("I am LP64\n");
#else
printf("I am ILP32 \n");
#endif
共享數(shù)據(jù)
在移植到64位平臺(tái)時(shí)的一個(gè)典型問題是,如何在32位和64位程序之間讀取和共享數(shù)據(jù)。例如一個(gè)32位程序可能把結(jié)構(gòu)體作為二進(jìn)制文件存儲(chǔ)在磁盤上,現(xiàn)在你要在64位代碼中讀取這些文件,很可能會(huì)因LP64環(huán)境中結(jié)構(gòu)大小的不同而導(dǎo)致問題。
對(duì)那些必須同時(shí)運(yùn)行在32位和64位平臺(tái)上的新程序而言,建議不要使用可能會(huì)因LP64和ILP32而改變長度的數(shù)據(jù)類型(如long),如果實(shí)在要用,可使用頭文件<inttypes.h>中的定寬整數(shù),這樣不管是通過文件還是網(wǎng)絡(luò),都可在32位和64位的二進(jìn)制層面共享數(shù)據(jù)。
例6:
#include <stdio.h>
#include <inttypes.h>
struct on_disk
{
/* ILP32|LP64共享時(shí),這個(gè)應(yīng)該使用int32_t */
long foo;
};
int main()
{
FILE *file;
struct on_disk data;
#ifdef WRITE
file=fopen("test","w");
data.foo = 65535;
fwrite(&data, sizeof(struct on_disk), 1, file);
#else
file = fopen("test","r");
fread(&data, sizeof(struct on_disk), 1, file);
printf("data: %ld\n", data.foo);
#endif
fclose(file);
}
來看一下例6,在理想的情況下,這個(gè)程序在32位和64位平臺(tái)上都可正常運(yùn)行,并且可以讀取對(duì)方的數(shù)據(jù)。但實(shí)際上卻不行,因?yàn)閘ong在ILP32和LP64之中長度會(huì)變化。結(jié)構(gòu)on_disk里的變量foo應(yīng)該聲明為int32_t,這個(gè)定寬類型可保證在當(dāng)前ILP32或移植到的LP64數(shù)據(jù)模型下,都生成相同大小的數(shù)據(jù)。
混合Fortran和C的問題
許多科學(xué)運(yùn)算程序從C/C++中調(diào)用Fortran的功能,F(xiàn)ortran從它本身來說并不存在移植到64位平臺(tái)的問題,因?yàn)镕ortran的數(shù)據(jù)類型有明確的比特大小。然而,如果混合Fortran和C語言,問題就來了,如下:例7中C語言程序調(diào)用例8中Fortran語言的子例程。
例7:
void FOO(long *l);
main ()
{
long l = 5000;
FOO(&l);
}
例8:
subroutine foo( i )
integer i
write(*,*) 'In Fortran'
write(*,*) i
return
end subroutine foo
例9:
% gcc -m64 -c cfoo.c
% /opt/absoft/bin/f90 -m64 cfoo.o foo.f90 -o out
% ./out
In Fortran
0
當(dāng)鏈接這兩個(gè)文件后,程序?qū)⒋蛴〕鲎兞縤的值為“5000”。而在LP64中,程序打印出“0”,因?yàn)樵贚P64模式下,子例程foo通過地址傳遞一個(gè)64位的參數(shù),而實(shí)際上,F(xiàn)ortran子例程想要的是一個(gè)32位的參數(shù)。如果要改正這個(gè)錯(cuò)誤,在聲明Fortran子例程變量i時(shí),把它聲明為INTEGER*8,此時(shí)和C語言中的long為一樣長度。
結(jié)論
64位平臺(tái)是解決大型復(fù)雜科學(xué)及商業(yè)問題的希望,大多數(shù)編寫良好的程序可輕松地移植到新平臺(tái)上,但要注意ILP32和LP64數(shù)據(jù)模型的差異,以保證有一個(gè)平滑的移植過程。