Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OC对象的内存对齐(2) #6

Open
lincf0912 opened this issue Jun 8, 2021 · 0 comments
Open

OC对象的内存对齐(2) #6

lincf0912 opened this issue Jun 8, 2021 · 0 comments

Comments

@lincf0912
Copy link
Owner

lincf0912 commented Jun 8, 2021

什么是内存对齐?

​ 元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始,这就是所谓的内存对齐。

下图是结构体在32bit和64bit环境下各基本数据类型所占的字节数:

image

为什么要内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

内存对齐规则

  1. 数据成员对齐规则:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照**#pragma pack**指定的数值和这个数据成员自身长度中,比较小的那个进行。
  2. 结构(或联合)的整体对齐规则:结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照**#pragma pack**指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
  3. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

#pragma pack(n) 对齐系数

​ 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
​ 其中,Xcode 中默认为**#pragma pack(8)

注:可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。对象生成的内存与系统开辟的内存关系

举一些例子

这里你只需要关注class_getInstanceSize的大小,暂时不用管malloc_size,与sizeof的值。

@interface TestObject : NSObject
@property(nonatomic,assign) int age;
@end

TestObject *test = [TestObject alloc];
test.age = 19;
  
NSLog(@"class_getInstanceSize = %zu",class_getInstanceSize([test class]));
NSLog(@"malloc_size = %lu",malloc_size(CFBridgingRetain(test)));
NSLog(@"sizeof = %lu",sizeof(test));
// 输出结果:
class_getInstanceSize = 16
malloc_size = 16
sizeof = 8
/*
每一个对象都有一个isa 占8字节
age int占4字节
8 + 4 = 12,属性默认是8字节对齐,意味着需要偏移量为4,12 + 4 = 16
*/

根据上面的例子,添加2个char属性。

@interface TestObject : NSObject
// *isa 8 (0-7)
@property(nonatomic,assign) int age;// 4 (8-12)
@property(nonatomic,assign) char c0;// 1 (13)
@property(nonatomic,assign) char c1;// 1 (14)
@end
/*
括号内为内存位置,(0-8)表示放置在0,1,2,3...7的位置上。首先按照规则1,2计算出来内存大小为14,再按照规则3,按照8的倍数对齐,则得出16个字节的结果。

输出为:
class_getInstanceSize = 16
malloc_size = 16
sizeof = 8
*/
  

调整属性的位置

@interface TestObject : NSObject
// *isa 8 (0-7)
@property(nonatomic,assign) char c0;// 1 (8-9)
@property(nonatomic,assign) int age;// 4 (12-16) /* 10不是4的倍数,偏移量为2 */
@property(nonatomic,assign) char c1;// 1 (17)
@end
/*
括号内为内存位置,(0-8)表示放置在0,1,2,3...7的位置上。首先按照规则1,2计算出来内存大小为17,再按照规则3,按照8的倍数对齐,则得出24个字节的结果。

输出为:
class_getInstanceSize = 24
malloc_size = 32
sizeof = 8
*/

看一下结构体的情况

struct AA {
    int a;   //长度4 < 8 按4对齐;偏移量为0;存放位置区间[0,3]
    char b;  //长度1 < 8 按1对齐;偏移量为4;存放位置区间[4]
    short c; //长度2 < 8 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
    char d;  //长度1 < 8 按1对齐;偏移量为7;存放位置区间[8],总大小为9
};
//整体对齐系数 = min((max(int,short,char), 8) = 4,将9提升到4的倍数,则为12。所以最终结果为12个字节

再来一个嵌套结构体

struct A {
    char e[2];      //长度1 < 8 按2对齐;偏移量为0;存放位置区间[0,1]
    short h;        //长度2 < 8 按2对齐;偏移量为2;存放位置区间[2,3]
    //结构体内部最大元素为double,偏移量为4,提升到8,所以从8开始存放接下来的struct A
    struct B {
        int a;      //长度4 < 8 按4对齐;偏移量为8;存放位置区间[8,11]
        double b;   //长度8 = 8 按8对齐;偏移量为12,提升到16;存放位置区间16,23]
        float c;    //长度4 < 8,按4对齐;偏移量为24,存放位置区间[24,27]
    };
    //整体对齐系数 = min((max(int,double,float), 8) = 8,将内存大小由28补齐到8的整数倍32。所以最终结果为32个字节。
};

分析libmalloc的源码

@interface TestObject : NSObject
@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) int age;
@property(nonatomic,assign) long height;
@property(nonatomic,copy) NSString *hobby;
@end
  
TestObject *test = [TestObject alloc];
test.name = @"apple";
test.age = 19;
test.hobby = @"football";
test.height = 180;
        
NSLog(@"对象生成的内存:%lu,系统开辟的内存:%lu",class_getInstanceSize([test class]),malloc_size((__bridge const void *)(test)));

//对象生成的内存:40,系统开辟的内存:48

由上面的结果知道,为什么对象生成的内存和系统开辟的内存是不一样的呢?

为了搞清楚还是需要用到上一篇文章OC底层-对象的alloc流程探究(1)里面源码的_class_createInstanceFromZone方法

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    //判断当前class或者superclass是否有.cxx_construct构造方法的实现
    bool hasCxxCtor = cls->hasCxxCtor();
    //判断当前class或者superclass是否有.cxx——destruct析构方法的实现
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    //通过进行内存对齐得到实例大小
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        //初始化实例的isa指针
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

过滤多余的代码后,得到实际需要分析的代码。

//通过进行内存对齐得到实例大小
size_t size = cls->instanceSize(extraBytes); // 返回内存大小40
//开辟内存,创建对象
obj = (id)calloc(1, size); // 调用libmalloc的源码

注意:calloc属于libmalloc的源码,objc的源码和malloc的源码是分开的。

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;
	}

malloc_zone_calloc 传入的 default_zone,执行 ptr = zone->calloc(zone, num_items, size)

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();
	    }
	    // 此时传进来的 zone 的类型是上面 calloc 传入的 default_zone,所以 zone->calloc 的调用实现要看 default_zone 的定义。
	    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;
	}

defaultzone->calloc 实际的函数实现为 default_zone_calloc

static void *
	default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) {
  		// 创建真正的 zone
	    zone = runtime_default_zone();
  		// 使用真正的 zone 进行 calloc
	    return zone->calloc(zone, num_items, size);
	}

通过控制台输出p zone->calloc得出调用nano_calloc方法

59d5f88987b44eab4e1f4e48e986119b

nano_calloc 的实现如下:

如果要开辟的空间小于 NANO_MAX_SIZE,则进行则进行nanozone_tmalloc,反之,就进行helper_zone流程

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;
	    }
	    // 如果要开辟的空间小于 NANO_MAX_SIZE 则进行nanozone_t的malloc。
	    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 */
	        }
	    }
	    // 否则就进行helper_zone的流程
	    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	    return zone->calloc(zone, 1, total_bytes);
	}

_nano_malloc_check_clear源码

其中segregated_next_block 就是指针内存开辟算法,目的是找到合适的内存并返回

slot_bytes是加密算法的(其目的是为了让加密算法更加安全,本质就是一串自定义的数字)

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;
    // 获取16字节对齐之后的大小,slot_key非常关键,为slot_bytes/16的值,也是数组的二维下下标
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    // 根据_os_cpu_number经过运算获取 mag_index(meta_data的一维索引)
    mag_index_t mag_index = nano_mag_index(nanozone);
    // 确定当前cpu对应的mag和通过size参数计算出来的slot,去对应metadata的链表中取已经被释放过的内存区块缓存
    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
    // 检测是否存在已经释放过,可以直接拿来用的内存,已经被释放的内存会缓存在 chained_block_s 链表
    // 每一次free。同样会根据 index 和slot 的值回去 pMeta,然后把slot_LIFO的指针指向释放的内存。
    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
    if (ptr) {
    ...
    // 如果缓存的内存存在,这进行指针地址检查等异常检测,最后返回
    // 第一次调用malloc时,不会执行这一块代码。
    } 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;
}

进入segregated_size_to_fit加密算法源码, 通过算法逻辑,可以看出,其本质就会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;
        //k + 15 >> 4 << 4 --- 右移 + 左移 -- 后4位抹零,类似于16的倍数,跟 k/16 * 16一样
	//---16字节对齐算法,小于16就成0了
	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;
}

从源码中看到,传进来的size是40,SHIFT_NANO_QUANTUM是4,NANO_REGIME_QUANTA_SIZE就是16,那么这里就是16字节对齐,因为传进来的是40,为了能够16字节对齐需要补齐所以得到的就是48。从上面的对象的字节对齐是8字节,为什么系统开辟的内存是16字节呢?因为8字节对齐的参考的是对象里面的属性,而16字节对齐的参考的是整个对象,因为系统开辟的内存如果只是按照对象属性的大小来的话,可能会导致内存溢出的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant