1. 程式人生 > >32位架構應用轉64位架構小結

32位架構應用轉64位架構小結

64位應用適配

簡介

同桌面系統架構一樣,蘋果公司從 iOS 7 開始採用64位的A7處理器。在相同的裝置上,執行相同的應用,支援64位的應用比支援32位的應用效能更高。

蘋果公司的A7處理器支援兩個不同的指令集,一個32位的 ARM 指令集,這是為了支援以前的處理器,另一個便是全新的64位的 ARM 指令集。這個全新的64位架構,不僅擁有更大的地址空間,而且其全新的線性指令集更是支援兩倍於以前的整型及浮點型暫存器。為了更好的利用64位處理器的效能,蘋果公司優化了 LLVM 編譯器,這樣,64位應用能夠更好的處理資料,而對於64位整數運算及 NEON 操作的效能提升更加明顯。所以,儘管32位應用在A7處理器上執行比在以前的處理器上執行更快,但是,將其轉換為64位應用後效能更佳。

當64位的應用執行在 iOS 上時,其指標及一些原本是32位長度的資料型別也會變為64位長度,這意味著更多記憶體的消耗,所以,若不謹慎處之,記憶體消耗可能抵消由32位轉64位所帶來的效能提高。

當 iOS 執行在64位機上時,其包含有32位與64位兩個不同版本的系統框架,但是減少系統記憶體的消耗,更快的載入應用,當裝置上執行的應用均為64位時,iOS 並不會將32位版本的系統框架載入至記憶體。所以,應用支援64位裝置對大家均有好處。

Xcode 構建應用時,所生成的二進位制機器碼是預設包含了32位和64位的,但這種混合構建的要求是,部署目標的 iOS 版本不低於 5.1.1 ,而其中64位的二進位制機器碼只能在64位裝置上執行,並且該裝置搭載的 iOS 系統版本必須是 7.0.3 或其之後。所以,對於已經存在的應用,應先升級到 iOS 7 或 iOS 7 之後,而後再在64位機上執行,對於新建應用,目標版本應為 iOS 7 或其之後並編譯32位和64位兩個版本。

iOS 應用於 OS X 應用架構大部分相同,一些通用的程式碼可以很容易的執行在兩個系統上,同樣,64位的升級過程也有諸多相似之處。其中指標和一些 C 語言基本型別從32位變為64位,與 NSIntegerCGFloat 相關的程式碼需要仔細檢查。

  • 確定所有函式呼叫有相應的原型宣告
  • 避免64位變數被意外賦32位型別而遭截斷
  • 確定在64位中的執行準確
  • 建立資料結構時,應保證其32位與64位兩個版本的結構一致

對於32位升級至64位的應用,應用虛擬機器測試兩個版本均正常,最後釋出前,用64位裝置測試正常後,才可釋出。

64位架構的主要變化

對資料型別的影響

為了使不同的程式碼整合後能夠正常執行,這些程式碼便需要遵循一些約定,其中包括基本資料型別的大小及格式,就如同一個程式碼片段中呼叫其他程式碼時使用的指令一樣,而編譯器便是基於這些約定實現的,故不同程式碼編譯成二進位制後,能夠整合在一起正常執行。而這些約定可參見

application binary interface (ABI)

iOS 應用依賴底層應用介面與系統架構,從 iOS 7 開始,一些 iOS 裝置使用64位處理器,並提供32位及64位的執行環境。而對於大多的應用,64位執行環境與32位執行環境有兩個重要的不同:

  • 在64位執行環境中,Cocoa Touch 框架使用的大多資料型別同 Objective-C 語言本身的資料型別一樣,他們的資料長度都增長了,並且有嚴格的記憶體對齊規則。
  • 在64位執行環境中,任何函式的呼叫,都需要相應的函式原型宣告

在 C 與 Objective-C 語言中,資料型別的大小及其在記憶體中的對齊方式並不是預定義好的,相反,每個平臺定義其構建的資料型別的長度。這就意味著,只要遵循語言標準中的嚴格定義,平臺就可以使得變數最大程度的適配底層硬體和作業系統。iOS 上的64位執行環境,改變了內部資料型別的大小,同樣,在更高層的 Cocoa Touch 架構中使用的資料型別長度也隨之改變。32位與64位系統有約定的名稱,分別為 ILP32LP64,並且編譯器在為64系統編譯程式碼時,會定義 __LP64__ 巨集變數。

32位與64位架構中的變數大小及對齊的長度差別如下表所示:

在 iOS 與 OS X 中,32位與64位並不影響浮點型資料的大小,他們在記憶體中的對齊方式也始終是自然對齊,但是對於 CGFloat 型別由 float(4 bytes) 型別變為了 double(8 bytes) 型別,這就使得 Quartz 架構及其他使用了 Core Graphics 型別的架構中的變量表示範圍更大,也更準確。另外,64位的 ARM 架構環境使用小端位元組序,這與支援 ARMv7 架構的 ARM 處理器使用的32位 iOS 執行環境相同。

資料型別長度的變化對應用的影響也是不容忽視的,在編譯同一個應用程式碼到32位與64位兩種不同的執行環境時,應注意這之間帶來的效能及相容問題。

  • 由於資料型別的不同而引起的記憶體壓力不同
  • 32位應用與64位應用資料交換問題
  • 兩種架構中,運算結果的偏差
  • 當長度長的資料型別的變數值賦值給了長度短的資料型別變數時,值會被截斷,這可能導致賦值前後值不相同

對方法呼叫的影響

對於方法呼叫,若不涉及組合語言,這些約定並不重要,但是在64位架構中,有時候最好清楚方法的呼叫過程。當方法被呼叫時,編譯器會將引數傳遞給被呼叫者,這裡,大多約定,相較於固定引數個數的方法,接收可變個引數的方法會被執行。每個方法必須有相應的原型宣告,方法轉換為不同版本,必須與原方法有相同的方法簽名。

編寫底層程式碼直接操控 Objective-C 的執行時的時候,再也不能直接方法物件的 isa 指標了,而是通過執行時方法獲取相關資訊。

因為32位與64位的指令集有很大不同,所以,若應用中包含有組合語言程式碼,則需要使用新的指令集重寫該部分程式碼。當然,因為64位架構的ARM的方法呼叫習慣與ARM標準並不完全一致,詳細理解應參考 iOS ABI Function Call Guide

升級應用到64位

要支援64位 ARM 架構,iOS 的最低版本不能低於5.1.1,並且應在 build setting 中的 Architectures 選項中選擇 Standard architectures(armv7,arm64) 選項,而後修改更新程式碼,使其相容兩個架構,最後測試併發布。

修改更新程式碼時,應注意以下幾點:

  • 不應將指標型別轉換為 int 型別

    為了便於運算,通常會有下面的寫法
    
    int *c = something passed in as an argument....
    int *d = (int *)((int)c + 4); // 錯誤寫法
    int *d = c + 1;               // 正確寫法
    
    在32位架構中,因為指標長度與 int 長度大小一樣,所以不會出錯,但是在64位架構中,
    指標長度比 int 長度長了4個位元組,所以這種寫法會導致指標地址遭截斷丟失。
    若非要進行指標轉整型,可使用 uintptr_t 型別替代。
    
  • 保證使用的變數型別始終相同

    如以下程式碼寫法
    
    long PerformCalculation(void);
    
    int  x = PerformCalculation(); // 不正確
    long y = PerformCalculation(); // 正確
    
    當函式的返回值變數型別長度大於接收的變數型別長度時,會導致返回值的高位被截斷,
    從而導致結果錯誤,雖然這種寫法在32機上不會出錯,但是在64位機上卻會出錯,
    而在 ANSI C 語言標準中並沒有 int 與 long 型別長度一致的規定,
    所以最好保持返回值型別與接收值得變數型別一致。
    通常,在工程的編譯選項中 -Wshorten-64-to-32 選項
    都是有效的(可在 build setting 中搜索,如下圖),
    若想發現更多潛在錯誤,可將選項 -Wconversion 選項置為有效。在程式設計中,
    注意 long ,NSInteger ,CFIndex ,size_t 等型別的正確使用,
    而且注意 fpos_t 與 off_t 型別的長度始終是64位的,不要給他們賦 int 型別的值。
    另外,列舉型別同樣可以定義列舉變數大小,所以其大小可能與預想的不一樣,
    故不應假定其資料的長度,而是在賦值時,將其賦值給合適的資料型別變數。
    

  • Cocoa Touch 中常用型別的變化

    在 Cocoa Touch 中,尤其是 Core Foundation 和 Foundation 中,
    常常獲取並使用 C 語言的變數及 Objective-C 的物件中的變數。
    從32位架構到64位架構,NSInteger 的長度也從32位變為了64位,
    所以不能認為 NSInteger 與 int 始終等長,在接收 NSInteger 型別的返回值時,
    應使用 NSInteger 型別的變數。例如,在使用 NSNumber 儲存或獲取資料時,
    使用 NSCoder 編碼或解碼資料時,均需注意不同架構帶來的影響。
    另外,對於使用 NSInteger 定義的常量值,如 NSNotFound 在不同架構中,
    表示的值不相同,要注意因此導致的錯誤。
    CGFloat 在64位架構中,長度也變為64位,
    所以不能將其簡單的等同於 float 或 double 型別,
    使用時要保持一致。如下錯誤與正確用法的對比:
    // 錯誤
    CGFloat value = 200.0;
    CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &value);
    
    // 正確
    CGFloat value = 200.0;
    CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &value);
    
  • C 及 C 的派生語言的符號位擴充套件規則

    C 語言及其相似語言,有一系列規則決定,當該變數被賦值給長度更大的變數時,變數的最高位(符號位)如何擴充套件,這些規則如下:

    1. 當無符號位變數擴充套件時,擴充套件位補零即可
    2. 有符號位變數擴充套件時,擴充套件位與符號位的值相同,即使擴充套件後的結果為無符號位變數
    3. 常量(除了被字首修改的,如 0x8L)的資料型別通常是能夠表示該常量的最短長度的資料型別。
        16進製表示的資料,可以是 int ,long ,long long 型別,可以是有符號或無符號型別。
        10進位制資料總是被當做有符號型別資料。
    4. 等長度的有符號位數與無符號位數相加的結果是無符號位數
    

    由於位擴充套件的規則,整數計算時,應注意32位機與64位機上執行結果的不同,如下面的例子:

    int a = -2;
    unsigned int b = 1;
    long c = a + b;
    long long d = c;
    

    這個程式碼在32位架構上執行後,結果是-1(0xFFFFFFFF),但是,在64位機上結果卻是4294967295(0x00000000FFFFFFFF),其運算過程如下:

    計算機中的運算是以資料補碼相加的形式進行的
    32位架構運算執行過程:
    a 的補碼是 0xFFFFFFFE
    b 的補碼是 0x000000001
    a + b = 0xFFFFFFFF
    得到結果的補碼形式,最高位為1,則結果是負值,
    將補碼取反碼再加1,得到原碼,為0x00000001,可知最終結果為-1
    
    64位架構運算執行過程:
    a 的補碼是 0xFFFFFFFE
    b 的補碼是 0x00000001
    a + b = 0xFFFFFFFF
    得到結果的補碼形式,最高位雖為1,但在64位架構中,int/unsigned int 為32位長度,
    long 為64位長度,所以要進行位擴充套件,根據擴充套件規則第4條,這個結果被當做無符號數進行擴充套件,
    所以擴充套件後結果為0x00000000FFFFFFFF,最高位為0,
    則結果是正值,補碼即為原碼,最終結果為4294967295。
    為相容兩種架構,可以在a與b相加之前,將b轉換為 long 型別變數(0x0000000000000001)。
    這樣,在64位架構中,不同長度型別的數值相加,短長度的型別會擴充套件為長度長的型別,
    所以執行加法操作前,a 會先擴充套件為 long 型別,即0xFFFFFFFFFFFFFFFE,
    與0x0000000000000001相加,得到結果0xFFFFFFFFFFFFFFFF,即-1。
    

    再比如下面的例子:

    unsigned short a = 1;
    unsigned long b = (a << 31);
    unsigned long long c = b;
    
    在32位架構中,結果正常為0x80000000,可是64位架構中結果為0xffffffff80000000,
    這是因為 short 型別在執行移位操作之前會先轉換為 int 型別,而且是有符號數,
    故將0x80000000賦值給64位的b時,會進行符號位擴充套件(如擴充套件規則第二條)。
    為了避免錯誤,應在移位操作之前,將a轉換為 long 型別。
    

    另外,不應認為資料型別的長度是不變的,在必要情況下,使用反碼可避免一些錯誤。

    function_name(long value)
    {
        long mask = ~0x3; // 0xfffffffc or 0xfffffffffffffffc
        return (value & mask);
    }
    

使用明確長度的資料型別及對齊方式

當需要在不同架構的機器上儲存,操作檔案或資料時,應保證資料的一致性。在C99標準中,定義了固定長度的變數型別,而不必在意底層硬體架構的區別。當我們自定義資料結構時,若必須確保該資料結構的長度是固定不變的,則本著不浪費記憶體的原則,選擇使用固定長度的資料型別。C99標準中明確範圍的整數型別如下表:

在64位執行環境中,64位長度的變數型別的對齊方式從4個位元組變為8個位元組,所以即使使用固定長度型別的變數,定義的資料結構在不同的執行環境中,也可能有不一樣的長度。如下面的資料結構:

struct bar {
    int32_t foo0;
    int32_t foo1;
    int32_t foo2;
    int64_t bar;
};

在32位架構中,因為這4個變數均是按4個位元組對齊,前3個變數共計12個位元組,所以變數 bar 則從距離資料結構起始地址12個位元組的位置開始儲存,所以資料結構的長度為20個位元組。但是在64位編譯器的編譯下,前3個變數按3個位元組對齊,仍然不變,而變數 bar 則按照8個位元組方式對齊,所以,編譯器會在變數 foo2 的後面補4個位元組,所以變數 bar 從距離資料結構起始地址的16個位元組處開始儲存,所以資料結構的長度為24個位元組。由此可見,相同的資料結構,並且是指定了型別長度的變數,最終在不同的架構上因為對齊位元組長度的不一致,而導致了資料結構長度的差別。

為了較少記憶體的消耗,在宣告結構體內的變數時,通常將資料型別按對齊位元組數從大到小依次宣告,這樣做可以儘可能的減少因對齊而進行的位元組填充。當然,也可以使用 pragma 命令改變對齊位元組數,如下:

#pragma pack(4)
struct bar {
    int32_t foo0;
    int32_t foo1;
    int32_t foo2;
    int64_t bar;
};
#pragma options align=reset

但是這種非自然對齊的方式,對效能影響較大,所以若非為相容32位系統等必要情況,不宜使用。另外,使用 malloc 分配變數記憶體時,不應該傳具體記憶體大小,而是應使用 sizeof 計算變數型別所佔記憶體的大小。

支援兩種執行環境的佔位符

對於類似 printf 的函式,由於資料型別的改變,使得事情變得繁雜,但是為了支援不同的執行環境,可以使用在標頭檔案 inttypes.h 中定義的巨集變數。

如下表,分別列出了標準佔位符與新增的佔位符

如下面的例程,給出了列印同指針大小範圍的整型變數與指標的方法

#include <inttypes.h>
void *foo;
intptr_t k = (intptr_t) foo;
void *ptr = &k;

printf("The value of k is %" PRIdPTR "\n", k);
printf("The value of ptr is %p\n", ptr);

列印結果類似於下
The value of k is 106102872259872
The value of ptr is 0x7fff5275cd80

注意函式及函式指標

對於32位與64位執行環境,其函式呼叫的處理方式是不同的,最大的區別是,有任意個引數的函式讀取引數時的命令是不同於擁有固定個引數的函式的。所以,要確保函式呼叫的正確性,讓其可以準確的找到其引數。為了使編譯器確定函式的引數個數是否是固定的,每個函式必須有函式的原型宣告。另外,函式指標的使用必須與函式原型保持一致,不能將指向固定個引數函式的函式指標轉換為任意個引數函式的函式指標,反之亦不行。

但是也有例外,在 Objective-C 的執行時中,使用 objc_msgSend 等類似的函式處理訊息時,不必遵循上述的規則,因為 Objective-C 的執行時是直接轉至方法的實現處,但是需要將 objc_msgSend 函式轉換與方法一致的函式宣告,這裡注意方法的函式宣告在編譯時,會自動插入兩個引數,這兩個引數是 id 與 SEL ,分別是類例項及方法選擇器。

在使用任意個引數的函式時,注意其引數是不會自動轉化的,如函式接收的引數型別是 long 而入參是 int 型別,那麼,其會接著讀取下一個引數的內容,相反,函式的引數型別是 int 而提供了 long 型別,則引數的剩餘部分會傳入下一個引數,這無疑會出錯。

優化記憶體效能

64位編譯器的改進,使得64位應用比32位應用執行效能更好,但同時,這種改進增大了指標及資料所佔記憶體的大小,這反過來影響了應用的效能,所以開發64位應用,一定要全面優化記憶體的使用。在優化前,先比較應用在32位與64位環境中執行的記憶體變化,哪裡的記憶體消耗有明顯增加,就優化哪裡的記憶體使用。

常見的記憶體使用問題

  1. 在 Foundation 等框架中提供了很多方便的特性,但他們的使用會消耗記憶體,如使用 NSDictionary 單獨儲存一對鍵值對比直接使用變數儲存要消耗更多記憶體,所以應避免這種低負載的使用方式。

  2. 壓縮資料的儲存,如使用多個變數儲存日期的年月日,不如儲存相較於某一個時間點的秒數,使用時再計算出具體時間

    struct date
    {
        NSInteger second;
        NSInteger minute;
        NSInteger hour;
        NSInteger day;
        NSInteger month;
        NSInteger year;
    };
    
    上面這種方式儲存時間,會佔用24個位元組,在64位環境中更是多達48個位元組,
    不如使用 ANSI C 所定義的 time_t 型別儲存秒數,但是注意其長度會因環境不同而改變。
    
    struct date
    {
        time_t seconds;
    };
    
  3. 在資料結構中,將對齊位元組數長的變數型別靠前宣告,儘量減少因對齊而填充的位元組數

  4. 儘量減少指標的使用,提高資料的負載

    struct node
    {
        node        *previous;
        node        *next;
        uint32_t    value;
    };
    
    這種結構,在64位環境中,記憶體的80%被指標使用,
    應儘量避免,而是使用陣列或其他集合型別儲存指標索引值。
  5. 使用 malloc 方法申請記憶體時,申請較大記憶體比單獨為每一個數據結構申請記憶體更有效,避免系統因對齊而填充位元組

  6. 必要時才使用快取

    使用快取可以提高效能,但是太依賴快取,造成虛擬記憶體系統壓力過大反而會降低效能,所以以下情況應避免使用快取:

    • 能夠輕易計算出來的資料
    • 能夠輕易從其他類中獲取的資料或物件
    • 能夠輕鬆重新生成的系統物件
    • 能夠通過 mmap() 訪問的只讀資料