天天看點

向64位進軍

向 64 位進軍   64位平台是解決複雜計算問題的曙光     随着低成本64位平台的來臨,加上記憶體和硬碟價格的不斷下跌,無疑為32位程式向64位硬體的移植又加了一把勁,那些科學運算、資料庫、消耗大量記憶體或密集浮點運算的程式也搭上了這一順風車。在本文中,主要讨論向64位平台移植現有32位代碼時,應注意的一些細小問題。 新近的64位平台在二進制上與32位應用程式相容,這意味着可以非常簡單地移植現有的程式。許多目前在32位平台上運作良好的程式也許不必移植,除非程式有以下要求: Ø 需要多于4GB的記憶體。 Ø 使用的檔案大小常大于2GB。 Ø 密集浮點運算,需要利用64位架構的優勢。 Ø 能從64位平台的優化數學庫中受益。 否則,隻需簡單地重新編譯一下,就已經足夠了。大多數編寫良好的程式不費吹灰之力就可移植到64位平台之上,在此假定你的程式編寫良好,并熟悉本文将要讨論的問題。     ILP32 和LP64 資料模型 32位環境涉及“ILP32 ”資料模型,是因為C資料類型為32位的int、long、指針。而64位環境使用不同的資料模型,此時的long和指針已為64位,故稱作“LP64 ”資料模型。 現今所有64位的類Unix平台均使用LP64資料模型,而64位Windows使用LLP64資料模型,除了指針是64位,其他基本類型都沒有變。我們在此主要探讨ILP32到LP64的移植問題,表1顯示了ILP32與LP64資料模型的差異。

向64位進軍

  向64位移植代碼時的所有問題差不多都可以總結出一個簡單的規律:千萬不要認為int、long、指針的長度一樣。任何違反這條規律的代碼,當運作在LP64資料模型下時,都會出現不同的問題,而且很難找出原因所在。例1中有許多違反這條規律的地方,其在移植到64位平台上時都需要重寫。   例1:  1       int *myfunc(int i)  2       {  3                return(&i);  4       }  5  6        int main(void)  7        {  8                int     myint;  9                long    mylong; 10               int     *myptr; 11 12         char *name = (char * ) getlogin(); 13 14                 printf("Enter a number %s: ", name); 15                (void) scanf("%d", &mylong); 16                 myint = mylong; 17                 myptr = myfunc(mylong); 18                printf("mylong: %d pointer: %x /n", mylong, myptr); 19                 myint = (int)mylong; 20                 exit(0); 21 22       }   第一步是要求編譯器捕捉到移植時的問題,因所用編譯器的不同,選項可能也有所不同,但對IBM XL編譯器系列,可用的選項有-qwarn64 -qinfo=pro,為了得到64位可執行檔案,可使用選項-q64(如果使用GCC,選項應為-m64,表2中列出了其他可用的GCC選項)。圖1是編譯例1中代碼時的情況。

向64位進軍
向64位進軍

  缺少原型的截斷 如果一個函數被調用時沒有指定函數原型,傳回值将是32位的int。不使用原型的代碼可能會發生意料之外的資料截斷,由此導緻一個分割錯誤。編譯器捕捉到了例1中第12行的這個錯誤。 char *name = (char *) getlogin(); 編譯器假定函數傳回一個int值,并截短結果指針。這行代碼在ILP32資料模型下工作正常,因為此時的int和指針是同樣長度,換到LP64模型中,就不一定正确了,甚至于類型轉換都不能避免這個錯誤,因為getlogin()在傳回之後已經被截斷了。 要修正這個問題,需包括頭檔案<unistd.h>,其中有getlogin()的函數原型。     格式指定符 如果對64位long、指針使用了32位格式指定符,将導緻程式錯誤。編譯器捕捉到了例1中第15行的這個錯誤。 (void) scanf("%d", &mylong); 注意,scanf将向變量mylong中插入一個32位的值,而剩下的4位元組就不管了。要修正這個問題,請在scanf中使用%ld指定符。 第18行也示範了在printf中的一個類似的問題: printf("mylong: %d pointer: %x /n", mylong, myptr); 要修正此處的錯誤,mylong應使用%ld,對myptr使用 %p而不是%x。     指派截斷 有關編譯器發現指派截斷的一個例子在第16行中: myint = mylong; 這在ILP32模型下不會有任何問題,因為此時的int、long都是32位,而在LP64中,當把mylong指派給myint時,如果數值大于32位整數的最大值時,數值将被截短。     被截斷的參數 編譯器發現的下一個錯誤在第17行中,雖然myfunc函數隻接受一個int參數,但調用時卻用了一個long,參數在傳遞時會悄無聲息地被截斷。     轉換截斷 轉換截斷發生在把long轉換成int時,比如說例1中的第19行: myint = (int) mylong; 導緻轉換截斷的原因是int與long非同樣長度。這些類型的轉換通常在代碼中以如下形式出現: int length = (int) strlen(str); strlen傳回size_t(它在LP64中是unsigned long),當指派給一個int時,截斷是必然發生的。而通常,截斷隻會在str的長度大于2GB時才會發生,這種情況在程式中一般不會出現。雖然如此,也應該盡量使用适當的多态類型(如size_t、uintptr_t等等),而不要去管它最下面的基類型是什麼。     一些其他的細小問題 編譯器可捕捉到移植方面的各種問題,但不能總指望編譯器為你找出一切錯誤。 那些以十六進制或二進制表示的常量,通常都是32位的。例如,無符号32位常量0xFFFFFFFF通常用來測試是否為-1: #define INVALID_POINTER_VALUE 0xFFFFFFFF 然而,在64位系統中,這個值不是-1,而是4294967295;在64位系統中,-1正确的值應為0xFFFFFFFFFFFFFFFF。要避免這個問題,在聲明常量時,使用const,并且帶上signed或unsigned。 const signed int INVALID_POINTER_VALUE = 0xFFFFFFFF; 這行代碼将會在32位和64位系統上都運作正常。   其他有關于對常量寫死的問題,都是基于對ILP32資料模型的不當認識,如下: int **p; p = (int**)malloc(4 * NO_ELEMENTS); 這行代碼假定指針的長度為4位元組,而這在LP64中是不正确的,此時是8位元組。正确的方法應使用sizeof(): int **p; p = (int**)malloc( sizeof(*p) * NO_ELEMENTS); 注意對sizeof()的不正确用法,例如: sizeof(int) = = sizeof(int *); 這在LP64中是錯誤的。     符号擴充 要避免有符号數與無符号數的算術運算。在把int與long數值作對比時,此時産生的資料提升在LP64和ILP32中是有差異的。因為是符号位擴充,是以這個問題很難被發現,隻有保證兩端的操作數均為signed或均為unsigned,才能從根本上防止此問題的發生。   例2: long k; int i = -2; unsigned int j = 1; k = i + j;   printf("Answer: %ld/n", k);   你無法期望例2中的答案是-1,然而,當你在LP64環境中編譯此程式時,答案會是4294967295。原因在于表達式(i+j)是一個unsigned int表達式,但把它指派給k時,符号位沒有被擴充。要解決這個問題,兩端的操作數隻要均為signed或均為unsigned就可。像如下所示: k = i + (int) j     聯合體問題(Union ) 當聯合本中混有不同長度的資料類型時,可能會導緻問題。如例3是一個常見的開源代碼包,可在ILP32卻不可在LP64環境下運作。代碼假定長度為2的unsigned short數組,占用了與long同樣的空間,可這在LP64平台上卻不正确。   例3: typedef struct {     unsigned short bom;     unsigned short cnt;     union {         unsigned long bytes;         unsigned short len[2];     } size; } _ucheader_t;   要在LP64上運作,代碼中的unsigned long應改為unsigned int。要在所有代碼中仔細檢查聯合體,以确認所有的資料成員在LP64中都為同等長度。     位元組序問題(Endian ) 因64位平台的差異,在移植32位程式時,可能會失敗,原因可歸咎于機器上位元組序的不同。Intel、IBM PC等CISC晶片使用的是Little-endian,而Apple之類的RISC晶片使用的是Big-endian;小尾位元組序(Little-endian)通常會隐藏移植過程中的截斷bug。   例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是一個有此問題的明顯例子,一個聲明指向int的指針,卻不經意間指向了long。在ILP32上,這段代碼列印出2,因為int與long長度一樣。但到了LP64上,因為int與long的長度不一,而導緻指針被截斷。不管怎麼說,在小尾位元組序的系統中,代碼依舊會給出k的正确答案2,但在大尾位元組序(Big-endian)系統中,k的值卻是0。

向64位進軍

  表3說明了為什麼在不同的位元組序系統中,會因截斷問題而産生不同的答案。在小尾位元組序中,被截斷的高位位址中全為0,是以答案仍為2;而在大尾位元組序中,被截斷的高位位址中包含值2,這樣就導緻結果為0,是以在兩種情況下,截斷都是一種bug。但要意識到,小尾位元組序會隐藏小數值的截斷錯誤,而這個錯誤隻有在移植到大尾位元組序系統上時才可能被發現。     移植到64 位平台之後的性能降低 當代碼移植到64位平台之後,也許發現性能實際上降低了。原因與在LP64中的指針長度和資料大小有關,并由此引發的緩存命中率降低、資料結構膨脹、資料對齊等問題。 由于64位環境中指針所占用的位元組更大,緻使原來運作良好的32位代碼出現不同程度的緩存問題,具體表現為執行效率降低。可使用工具來分析緩存命中率的變化,以确認性能降低是否由此引起。 在遷移到LP64之後,資料結構的大小可能會改變,此時程式可能會需要更多的記憶體和磁盤空間。例如,圖2中的結構在ILP32中隻需要16位元組,但在LP64中,卻需要32位元組,整整增長了100%。這緣于此時的long已是64位,編譯器為了對齊需要而加入了額外的填充資料。

向64位進軍

  通過改變結構中資料排列的先後順序,能将此問題所帶來的影響降到最小,并能減少所需的存儲空間。如果把兩個32位int值放在一起,會因為少了填充資料,存儲空間也随之減少,現在存儲整個結構隻需要24位元組。 在重排資料結構之前,在根據資料使用的頻度仔細衡量,以免因降低緩存命中率而帶來性能上的損失。     如何生成64 位代碼 在一些情況中,32位和64位程式在源代碼級别的接口上很難區分。不少頭檔案中,都是通過一些測試宏來區分它們,不幸的是,這些特定的宏依賴于特定的平台、特定的編譯器或特定的編譯器版本。舉例來說,GCC 3.4或之後的版本都定義了__LP64__,以便為所有的64位平台通過選項-m64編譯産生64位代碼。然而,GCC 3.4之前的版本卻是特定于平台和作業系統的。 也許你的編譯器使用了不同于__LP64__的宏,例如IBM XL的編譯器當用-q64編譯程式時,使用了__64bit__宏,而另一些平台使用_LP64,具體情況可用__WORDSIZE來測試一下。請檢視相關編譯器文檔,以便找出最适合的宏。例5可适用于多種平台和編譯器:   例5: #if defined (__LP64__) || defined (__64BIT__) || defined (_LP64) || (__WORDSIZE == 64)    printf("I am LP64/n"); #else    printf("I am ILP32 /n"); #endif     共享資料 在移植到64位平台時的一個典型問題是,如何在32位和64位程式之間讀取和共享資料。例如一個32位程式可能把結構體作為二進制檔案存儲在磁盤上,現在你要在64位代碼中讀取這些檔案,很可能會因LP64環境中結構大小的不同而導緻問題。 對那些必須同時運作在32位和64位平台上的新程式而言,建議不要使用可能會因LP64和ILP32而改變長度的資料類型(如long),如果實在要用,可使用頭檔案<inttypes.h>中的定寬整數,這樣不管是通過檔案還是網絡,都可在32位和64位的二進制層面共享資料。   例6: #include <stdio.h> #include <inttypes.h>   struct on_disk {       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,在理想的情況下,這個程式在32位和64位平台上都可正常運作,并且可以讀取對方的資料。但實際上卻不行,因為long在ILP32和LP64之中長度會變化。結構on_disk裡的變量foo應該聲明為int32_t,這個定寬類型可保證在目前ILP32或移植到的LP64資料模型下,都生成相同大小的資料。     混合Fortran 和C 的問題 許多科學運算程式從C/C++中調用Fortran的功能,Fortran從它本身來說并不存在移植到64位平台的問題,因為Fortran的資料類型有明确的比特大小。然而,如果混合Fortran和C語言,問題就來了,如下:例7中C語言程式調用例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   當連結這兩個檔案後,程式将列印出變量i的值為“5000”。而在LP64中,程式列印出“0”,因為在LP64模式下,子例程foo通過位址傳遞一個64位的參數,而實際上,Fortran子例程想要的是一個32位的參數。如果要改正這個錯誤,在聲明Fortran子例程變量i時,把它聲明為INTEGER*8,此時和C語言中的long為一樣長度。     結論 64位平台是解決大型複雜科學及商業問題的希望,大多數編寫良好的程式可輕松地移植到新平台上,但要注意ILP32和LP64資料模型的差異,以保證有一個平滑的移植過程。    

繼續閱讀