深入理解Tagged Pointer

前言

在2013年9月,蘋果推出了iPhone5s,與此同時,iPhone5s配備了首個採用64位架構的A7雙核處理器,爲了節省內存和提高執行效率,蘋果提出了Tagged Pointer的概念。對於64位程序,引入Tagged Pointer後,相關邏輯能減少一半的內存佔用,以及3倍的訪問速度提升,100倍的創建、銷燬速度提升。本文從Tagged Pointer試圖解決的問題入手,帶領讀者理解Tagged Pointer的實現細節和優勢,最後指出了使用時的注意事項。

問題

我們先看看原有的對象爲什麼會浪費內存。假設我們要存儲一個NSNumber對象,其值是一個整數。正常情況下,如果這個整數只是一個NSInteger的普通變量,那麼它所佔用的內存是與CPU的位數有關,在32位CPU下佔4個字節,在64位CPU下是佔8個字節的。而指針類型的大小通常也是與CPU位數相關,一個指針所佔用的內存在32位CPU下爲4個字節,在64位CPU下也是8個字節。

所以一個普通的iOS程序,如果沒有Tagged Pointer對象,從32位機器遷移到64位機器中後,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的對象所佔用的內存會翻倍。如下圖所示:

我們再來看看效率上的問題,爲了存儲和訪問一個NSNumber對象,我們需要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失。

Tagged Pointer

爲了改進上面提到的內存佔用和效率問題,蘋果提出了Tagged Pointer對象。由於NSNumber、NSDate一類的變量本身的值需要佔用的內存大小常常不需要8個字節,拿整數來說,4個字節所能表示的有符號整數就可以達到20多億(注:2^31=2147483648,另外1位作爲符號位),對於絕大多數情況都是可以處理的。

所以我們可以將一個對象的指針拆成兩部分,一部分直接保存數據,另一部分作爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。所以,引入了Tagged Pointer對象之後,64位CPU下NSNumber的內存圖變成了以下這樣:

對此,我們也可以用 Xcode做實驗來驗證。我們的實驗代碼如下:

int main(int argc, char * argv[])
{
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *numberFFFF = @(0xFFFF);

        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"numberffff pointer is %p", numberFFFF);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在該代碼中,我們將幾個Number類型的指針的值直接輸出。需要注意的是,我們需要將模擬器切換成 64位的CPU來測試,如下圖所示:

運行之後,我們得到的結果如下,可以看到,除去最後的數字最末尾的2以及最開頭的0xb,其它數字剛好表示了相應NSNumber的值。

number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberFFFF pointer is 0xb0000000000ffff2

可見,蘋果確實是將值直接存儲到了指針本身裏面。我們還可以猜測,數字最末尾的2以及最開頭的0xb是否就是蘋果對於Tagged Pointer的特殊標記呢?我們嘗試放一個8字節的長的整數到NSNumber實例中,對於這樣的實例,由於Tagged Pointer無法將其按上面的壓縮方式來保存,那麼應該就會以普通對象的方式來保存,我們的實驗代碼如下:

NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);

運行之後,結果如下,驗證了我們的猜測,bigNumber的地址更像是一個普通的指針地址,和它本身的值看不出任何關係:

bigNumber pointer is 0x10921ecc0

可見,當8字節可以承載用於表示的數值時,系統就會以Tagged Pointer的方式生成指針,如果8字節承載不了時,則又用以前的方式來生成普通的指針。關於以上關於Tag Pointer的存儲細節,我們也可以在這裏找到相應的討論,但是其中關於Tagged Pointer的實現細節與我們的實驗並不相符,筆者認爲可能是蘋果更改了具體的實現細節,並且這並不影響Tagged Pointer我們討論Tagged Pointer本身的優點。

特點

我們也可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對於Tagged Pointer特點的介紹:

  1. Tagged Pointer專門用來存儲小的對象,例如NSNumberNSDate
  2. Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中,也不需要malloc和free。
  3. 在內存讀取上有着3倍的效率,創建時比以前快106倍。

由此可見,蘋果引入Tagged Pointer,不但減少了64位機器下程序的內存佔用,還提高了運行效率。完美地解決了小內存對象在存儲和訪問效率上的問題。

isa指針

Tagged Pointer的引入也帶來了問題,即Tagged Pointer因爲並不是真正的對象,而是一個僞對象,所以你如果完全把它當成對象來使,可能會讓它露馬腳。比如我在《Objective-C對象模型及應用》一文中就寫道,所有對象都有 isa 指針,而Tagged Pointer其實是沒有的,因爲它不是真正的對象。 因爲不是真正的對象,所以如果你直接訪問Tagged Pointerisa成員的話,在編譯時將會有如下警告:

對於上面的寫法,應該換成相應的方法調用,如 isKindOfClass 和 object_getClass。只要避免在代碼中直接訪問對象的isa變量,即可避免這個問題。

總結

蘋果將Tagged Pointer引入,給64位系統帶來了內存的節省和運行效率的提高。Tagged Pointer通過在其最後一個bit位設置一個特殊標記,用於將數據直接保存在指針本身中。因爲Tagged Pointer並不是真正的對象,我們在使用時需要注意不要直接訪問其isa變量。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章