iOS底層探索之內存對齊和calloc

之前通過 objc 的源碼探索了 alloc 的內部流程,到最後會調用 size = cls->instanceSize(extraBytes); 方法,獲取內存大小,但是這個大小到底是怎麼計算的呢?

獲取大小後,會調用 calloc(1, size) 方法開闢內存大小,開闢的時候又有什麼不同呢?

這次就繼續探索一下系統的內存分配。

一、屬性所佔內存計算

從應用代碼開始

@interface GLPerson : NSObject
// 會有一個隱藏屬性 isa  佔8個字節
@property (nonatomic, copy  ) NSString *name; // 8
@property (nonatomic, assign) int height; // 4
@property (nonatomic, assign) char char1; // 1
@property (nonatomic, assign) char char2; // 1

@en

---
        GLPerson *p = [[GLPerson alloc] init];
        p.height = 180;
        p.name = @"loong";
        p.char1 = 'g';
        p.char2 = 'n';
        NSLog(@"%zd %zd", class_getInstanceSize([GLPerson class]), malloc_size((__bridge const void *)(p)));

上面會輸出:24 32

在alloc流程中簡單說過,屬性內存分配的時候是8字節對齊,GLPerson類的實例所佔大小計算爲 8 (isa) + 8 (name) + 4 (height) + 1 (char1) + 1 (char1) == 22。(模擬計算結果,實際在跟源碼的時候蘋果有內存優化,會把 4 (height) + 1 (char1) + 1 (char1) 放到一個8字節裏面存儲,這樣避免了浪費)

然後會對22進行8字節對齊,得到的是24。

// 1 
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
 // 2
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
// 3
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}
// 4
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

根據上面4個方法的調用順序

  1. class_getInstanceSize : 內部調用 alignedInstanceSize 返回
  2. alignedInstanceSize : 內部調用 word_align(unalignedInstanceSize()) 返回
  3. unalignedInstanceSize : 內部調用 data()->ro()->instanceSize返回,data()ro()的數據會在 loadImage 的時候完成,instanceSize會根據類有多少屬性,返回已經經過編譯器優化存儲後的結果
  4. word_align : 這個會對實例大小做8字節對齊,會返回8的倍數大小
1.1 對齊計算

word_align 就是8字節對齊計算

#   define WORD_MASK 7UL

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

可以看出word_align的對齊計算是 (x + 7) & ~7

1.2 計算結構體大小

咱們知道OC繼承與C,並且 struct objc_class : objc_objectobjc_class 的源碼也是一個結構體,結構體在計算大小的時候有3個原則:

1、每個成員的偏移量都必須是當前成員所佔內存大小的整數倍如果不是編譯器會在成員之間加上填充字節。
2、結構體作爲成員: 如果⼀個結構⾥有某些結構體成員,則結構體成員要從其內部最⼤元素⼤⼩的整數倍地址開始存儲。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b應該從8的整數倍開始存儲)
3、當所有成員大小計算完畢後,編譯器判斷當前結構體大小是否是結構體中最寬的成員變量大小的整數倍 如果不是會在最後一個成員後做字節填充。

struct structA {
    long height;    // 8
    int age;        // 4
    char char1;     // 1
    short short1;   // 2
};

struct structB {
    int age;        // 4
    long height;    // 8
    char char1;     // 1
    short short1;   // 2
};

struct structC {
    int age;        // 4
    struct structB sb;
    char sex; // 1
};
---
        struct structA a = {
            12, 20, 'a', 123
        };
        struct structB b = {};
        struct structC c = {};
        
        NSLog(@"A:%lu, B:%lu, C:%lu", sizeof(a), sizeof(b), sizeof(c));
---
console: A:16, B:24, C:40

structA:
height 0-->7;
age 8 --> 11;
char1 12;
short1 (根據原則1,第13位不是2的整數倍,往後移,14滿足) 14-->15;
實際總共:0--> 15 爲 16,
再根據原則3,需要是8的倍數,16滿足,最後就是16。

structB:
age 0 --> 3;
height (根據原則1,4不滿足8的整數倍,往後移,8滿足) 8-->15;
char1 16;
short1 (第17位不是2的整數倍,往後移,18滿足)18-->19;
實際總共:0--> 19 爲 20,
再根據原則3,需要是8的倍數,所以最後是24。

structC:
age 0 --> 3;
structB sb (根據原則2,因爲structB裏面最大的是8字節,4不滿足8的整數倍,往後移,8滿足,可知structB佔24) 8-->31;
sex 32;
實際總共:0--> 32 爲 33,
再根據原則3,需要是8的倍數,所以最後是40。

1.3 編譯優化

測試如下代碼

struct structB {
    long isa;       // 8
    char char1;     // 1
    int height;     // 4
    char char2;     // 1
    double name;    // 8
    char char3;     // 1
    char char4;     // 1
};
---
@interface GLPerson : NSObject
// 默認屬性isa  佔8個字節
@property (nonatomic, assign) char char1;       // 1
@property (nonatomic, assign) int height;       // 4
@property (nonatomic, assign) char char2;       // 1
@property (nonatomic, copy  ) NSString *name;   // 8
@property (nonatomic, assign) char char3;       // 1
@property (nonatomic, assign) char char4;       // 1
@end
---
        struct structB b = {};

        LGPerson *p = [[LGPerson alloc] init];
        p.char1 = 'a';
        p.height = 180;
        p.char2 = 'b';
        p.name = @"loong";
        p.char3 = 'c';
        p.char4 = 'd';

NSLog(@"%lu, %zd, %zd",sizeof(b), class_getInstanceSize([LGPerson class]), malloc_size((__bridge const void *)(p)));

console: 40, 24, 32

通過輸出結果可以知道,structB大小爲40,LGPerson真正佔用大小爲24。爲什麼,這裏面就是系統底層做了編譯優化,在字節對齊的基礎上,又節省了空間。可以通過打印內存地址查看

可以使用x 或者 memory read命令查看某個對象的內存情況。

更方便的查看4xg規則

可以通過po命令打印出內存中對應的值如下:

可以知道底層把 height char1 char2 char3 char4 放到了一個8字節裏面。
這樣不會像結構體那樣按順序存儲,中間會有很多補位。在保證對齊原則的情況下,極大的節省了內存空間。

二、calloc() 源碼分析

分析 malloc 的源碼,官方地址,本次分析的是libmalloc-283.100.6版本。

        void *p = calloc(1, 40);
        NSLog(@"%lu",malloc_size(p));

console: 48

爲什麼開闢了48???從源碼裏面找下答案

分析 malloc 源碼的時候,還是需要配置一個能編譯運行的源碼工程的,這樣斷點能走進去,方便分析。

2.1 calloc

calloc開始

void *
calloc(size_t num_items, size_t size)
{
    void *retval;
        // 主流程
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
        errno = ENOMEM;
    }
    return retval;
}

void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
        internal_check();
    }
        // 主流程
    ptr = zone->calloc(zone, num_items, size);
    
    if (malloc_logger) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
}

calloc -> malloc_zone_calloc -> ptr = zone->calloc(zone, num_items, size)

發現 calloc 又調回去了???只看源碼的話,確實找不到下一步走向了哪裏。

還是需要編譯運行工程,通過調用 void *p = calloc(1, 40);ptr = zone->calloc(zone, num_items, size) 打個斷點。然後打印下,看看調用方法

也可以通過按住 control 點擊 step into 多次,進入下一個方法調用。

可知下一步來到了 default_zone_calloc

2.2 default_zone_calloc 、nano_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    // 主流程
    return zone->calloc(zone, num_items, size);
}

同樣的方式,來到 nano_calloc

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;

    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }

    if (total_bytes <= NANO_MAX_SIZE) {
               // 主流程
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
        }
    }
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}

接着會走到 _nano_malloc_check_clear

2.3 _nano_malloc_check_clear
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

    void *ptr;
    size_t slot_key;
      // 在此處segregated_size_to_fit進行16字節對齊
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    mag_index_t mag_index = nano_mag_index(nanozone);

    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
        // 通過斷點發現pMeta爲0x0, ptr爲NULL,會走到else裏面
    if (ptr) {
        ...
          // 中間省略很多代碼
    } else {
        ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
    }

    if (cleared_requested && ptr) {
        memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
    }
    return ptr;
}

這個時候我發現,size竟然變成了18

通過線程可以看到第一次進來的調用順序

發現是調用了 _malloc_initialize_once 方法。這個先跳過,

然後繼續往下走

會走到 ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index); 這個方法裏面就是查找能開闢給定 slot_bytes 大小的內存的地方。直到查到返回。

static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
    while (1) {
        uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
        uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
        b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.

        if (b < theLimit) {   // Did we stay within the bound of the present slot allocation?
            return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
        } else {
            if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
                pMeta->slot_bump_addr = theLimit;
                return 0;                // We're toast
            } else {
                // One thread will grow the heap, others will see its been grown and retry allocation
                _malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
                // re-check state now that we've taken the lock
                if (pMeta->slot_exhausted) {
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    return 0; // Toast
                } else if (b < pMeta->slot_limit_addr) {
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
                } else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    continue; // ... the slot has been successfully grown by us. Now try again.
                } else {
                    pMeta->slot_exhausted = TRUE;
                    pMeta->slot_bump_addr = theLimit;
                    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                    return 0;
                }
            }
        }
    }
}

那什麼時候進行的16字節對齊的呢?

發現在查找地址之前有個方法 size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); 這個方法返回後,slot_bytes就成了48了(16字節對齊過了)

2.4 segregated_size_to_fit -- 16字節對齊

16字節對齊方法

#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

通過計算發現,和上面講的8字節對齊是不是道理一樣,先給你補個差額(15),
然後通過右移4位,把 24 以下的二進制位幹掉,
再左移4位,恢復原來的高二進制位的數據。
從而達到16字節對齊


END

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