swift之内存布局

struct和tuple内存布局

结构体和元组当前共享相同的布局算法,在编译器实现中称为“通用”布局算法。算法如下:

  • 一开始设置size为0,alignment为1

  • 遍历字段,对于每个字段:

    • 先根据这个字段的alignment来更新size,让这个字段能够对齐

    • size增加这个字段的大小

    • 更新alignment为 Max(alignment,这个字段的alignment)

  • 最终拿到总的size和alignment,然后size根据alignment对其,得到strip。

比如:

structPerson{
   var age:Int64=0
   var sex:UInt16=0
   var address:Double=0.0
   var name:UInt8=0
}

开始设置size=0,alignment=1
age是8字节对其,size为0,没问题,不需要调整。
age是8字节,size增加8
age的alignment为8,更新alignment为Max(1,8)
sex为2字节对其,size为8,没问题,不用调整。
sex是2字节,size增加2,为10
sex的alignment为2,更新alignment为Max(8,2)
address为8字节对其,size为10,需调整size为16,来保证对其。
address是8字节,size增加8,为24
address的alignment为8,更新alignment为Max(8,8)
name为1字节对其,size为24,不用调整。
name是1字节,size增加1,为25
name的alignment为1,更新alignment为Max(8,1)
所以size为25, alignment为8, 调整strip为32,保证对其。</pre>

Class Layout

参考https://academy.realm.io/posts/goto-mike-ash-exploring-swift-memory-layout/, 里面有一个探索内存的工具https://github.com/mikeash/memorydumper2。我们传一个变量给他,他能分析出这个是一个指针还是其他东西,并给出关系图谱,不过需要安装Graphviz。

class是引用类型,因此,我们定义一个变量拿到的是这个实例变量在内存中的引用。

classPerson{
   var age=0x0101010101010101
   var money= 0x0202020202020202
}
letp=Person()

那么p指向的实例对象的内存模式大概长什么样呢?

image

现在还看不懂,我们先介绍下swift源码中的一些数据结构。

HeapObject

swift中所有分配在堆上的东西都是一个HeapObject。我们看看HeapObject的定义:

/** 我们看到其实就两个变量,一个metadata,和一个InlineRefCounts */
struct HeapObject{
 /// This is always a valid pointer to a metadata object.
 HeapMetadataconst*metadata;
​
 InlineRefCounts refCounts;
};

HeapMetadata

HeapMetadata是类结构体的指针。

//===============HeapMetadata=============
template<typenameTarget>structTargetHeapMetadata;
usingHeapMetadata=TargetHeapMetadata<InProcess>;
//================TargetHeapMetadata==============
//继承自TargetMetadata,并且有两个初始化方法,通过Kind初始化和通过TargetAnyClassMetadata初始化
template<typenameRuntime>
structTargetHeapMetadata: TargetMetadata<Runtime>{
 usingHeaderType=TargetHeapMetadataHeader<Runtime>;
​
 TargetHeapMetadata() =default;
 constexprTargetHeapMetadata(MetadataKindkind)
  : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
 constexprTargetHeapMetadata(TargetAnyClassMetadata<Runtime>*isa)
  : TargetMetadata<Runtime>(isa) {}
#endif
};
//=================Kind==========
enumclassMetadataKind: uint32_t{
#define METADATAKIND(name, value) name = value,
#define ABSTRACTMETADATAKIND(name, start, end)                                 \
 name##_Start = start, name##_End = end,
#include "MetadataKind.def"
   /** 这是从MetadataKind.def截取的一段,可以看到就是声明了各种Kind的枚举
NOMINALTYPEMETADATAKIND(Struct, 1)
NOMINALTYPEMETADATAKIND(Enum, 2)
NOMINALTYPEMETADATAKIND(Optional, 3)
METADATAKIND(Opaque, 8)
METADATAKIND(Tuple, 9)
METADATAKIND(Function, 10)
METADATAKIND(Existential, 12)
METADATAKIND(Metatype, 13)
METADATAKIND(ObjCClassWrapper, 14)*/
 LastEnumerated=2047,
};
//================TargetAnyClassMetadata==========
//
template<typenameRuntime>
structTargetAnyClassMetadata: publicTargetHeapMetadata<Runtime>{
....
 ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata>Superclass;
 TargetPointer<Runtime, void>CacheData[2];
 StoredSizeData;
.....
};
structTargetHeapMetadata: TargetMetadata<Runtime>{
....
};
structTargetMetadata{
   //StoredPointer是不同平台上指针大小的类型,32位上相当于int32,64位上为int64,他要么是一个MetaKind枚举,要么是一个isa指针。
StoredPointerKind;
}
综上我们再看看TargetAnyClassMetadata都有些什么:
{
 StoredPointerKind;
 ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata>Superclass;
 TargetPointer<Runtime, void>CacheData[2];
 StoredSizeData;
}
熟悉么?没什么感觉的话,我们再看看oc的class的定义:
structobjc_class{
   ClassISA;//对应Kind
   Classsuperclass;//对应Superclass
   cache_tcache;   //对应cachedata
   class_data_bits_tbits; //对应Data
}

综上,我们看到HeapMetadata就是一个类结构体。代表这个实例的Class。HeapObject中的第一个变量HeapMetadata const *metadata就是一个指向Class对象的指针。

InlineRefCounts

typedef RefCounts<InlineRefCountBits>InlineRefCounts;
​
template<typenameRefCountBits>
classRefCounts{
 std::atomic<RefCountBits>refCounts;
 /** 各种操作引用计数的方法 */
 incrementSlow()
 tryIncrementAndPinSlow
 decrementShouldDeinit
.....
}

从上可以看出InlineRefCounts就是InlineRefCountBits,并提供了各种操作引用计数的方法。

typedef RefCountBitsT<RefCountIsInline>InlineRefCountBits;
​
template<RefCountInlinednessrefcountIsInline>
classRefCountBitsT{
 //可能包含的是引用计数,也可能包含的是sidetable的指针
 BitsTypebits;
 RefCountBitsT(HeapObjectSideTableEntry*side)
 RefCountBitsT(uint32_tstrongExtraCount, uint32_tunownedCount);
 //封装了对各个bit的存储的操作
 getUnownedRefCount
 getIsPinned
 getIsDeiniting
 。。。。。
}

从上可以看到,refCounts是一个RefCountBitsT类,类中有一个bits。如果没有sidetable的情况,那么引用计数会记录在这里面,如果有sidetable,那么包含的是HeapObjectSideTableEntry的指针。

//看看每一位表示什么(64位情况)
template<>
structRefCountBitOffsets<8>{
 static const size_t IsPinnedShift=0;
 static const size_tIsPinnedBitCount=1;
 static const uint64_tIsPinnedMask=maskForField(IsPinned);
​
 static const size_tUnownedRefCountShift=shiftAfterField(IsPinned);
 static const size_tUnownedRefCountBitCount=31;
 static const uint64_tUnownedRefCountMask=maskForField(UnownedRefCount);
​
 static const size_tIsDeinitingShift=shiftAfterField(UnownedRefCount);
 static const size_tIsDeinitingBitCount=1;
 static const uint64_tIsDeinitingMask=maskForField(IsDeiniting);
​
 static const size_tStrongExtraRefCountShift=shiftAfterField(IsDeiniting);
 static const size_tStrongExtraRefCountBitCount=30;
 static const uint64_tStrongExtraRefCountMask=maskForField(StrongExtraRefCount);
​
 static const size_tUseSlowRCShift=shiftAfterField(StrongExtraRefCount);
 static const size_tUseSlowRCBitCount=1;
 static const uint64_tUseSlowRCMask=maskForField(UseSlowRC);
​
 static const size_tSideTableShift=0;
 static const size_tSideTableBitCount=62;
 static const uint64_tSideTableMask=maskForField(SideTable);
 static const size_tSideTableUnusedLowBits=3;
​
 static const size_tSideTableMarkShift=SideTableBitCount;
 static const size_tSideTableMarkBitCount=1;
 static const uint64_tSideTableMarkMask=maskForField(SideTableMark);
};
===============================
低位---》高位
1位         IsPinned
31位        UnownedRefCountBit
1位         IsDeiniting
30位        StrongExtraRefCount
1位         UseSlowRC
如果是有sidetable的情况,各个bit表示如下:
62位        SideTable指针
1位         SideTableMark
1位         UseSlowRC

再看对象内存布局

再看看一开始的图:

image

E87b5600100000:是Class的地址。

02000000000000:是inlineref。引用计数,不过怎么都和看到的offset对不上。02那个是00000010,其中的1是代表unowned ref的数量,我们可以验证下:

class Person{
   var age=0x0102030405060708
   var money= 0x0202020202020202
   func sayHello() {
       print("hello")
   }
   func sayWorld() {
       print("world")
   }
}
let p1=Person()
let p2=p1
let p3=p1
let p4=p1
let p5=p1
unowned var p6=p1
unowned var p7=p1
dumpAndOpenGraph(dumping:p1,maxDepth:10,filename:"SimpleClassPerson")

上面有5个额外强引用,2个额外无主引用(总数为2+1,因为初始值为1),虽然不知道具体的offset规则,不过我们可以肯定,一定有一个1010和一个11子序列,代表5个额外强引用和3个无主引用:

image

可以看到06(00000110),其中11位无主引用数3,0a(00001010),其中101为额外强引用计数。

ok,我们在试试定义一个weak,让对象有sidetable(因为weak引用计数在sidetable的SideTableRefCountBits中)。

weak var p8=p1
image

可以看到ref已经变味一个指针+flag。

Class的layout

image

这是Person这个Class中的信息,读取的信息描述也不是很清晰,我们大概可以认为他是一个特殊的vtable,混杂了一些oc类结构体中的一些信息。

Class{
   isa
   supperclass
   cache
  各种method指针
}
image

这是放大的部分,128字节开始是sayHello函数,136字节开始是sayWorld函数,还有各种getter和setter等。

image

image

第8字节开始,也就数superclass,可以看到,我们定义的Person是SwiftObject的子类。SwiftObject是一个实现了NSObject协议的OC类。

枚举的内存布局

在layout枚举时,ABI为了避免浪费空间,会从以下的5种策略中选择。

Empty Enums

enum Empty{}// => empty type
此时size为0, alignment为1, 为了对齐strip为1

Single-Case Enums

如果enum只有一个case,那么关联了什么data就怎么布局,如果没有关联,那么为empty(因为只有一个case不需要区分)。

enum EmptyCase{caseX}            // => empty type
enum DataCase{caseY(Int,Double)}// => LLVM <{ i64, double }>
EmptyCasesize为0,alignment为1,strip为1
DataCase布局就是1个Int,Double。所以size为16,alignment为8,strip为16

C-Like Enums

如果所有case都没有关联data type,那么这就是一个c-like enum。enum布局就是一个整数tag,用最少的bit来描述所有case。

enum EnumLike2{// => LLVM i1
 case A        // => i1 0
 case B        // => i1 1
}
​
enum EnumLike8{// => LLVM i3
 case A        // => i3 0
 case B        // => i3 1
 case C        // => i3 2
 case D        // etc.
 case E
 case F
 case G
 case H
}
size为1, alignment为1,strip为1。为什么是1呢,因为1个字节可以表示2的8次方种情况,足够表示这么多case了。那超过了呢?
又比如
enum A{
   case A0
   case A1
   ....
   case A280
}
此时不足以用1个字节表示所以,size为2,alignment为2,strip为2.

Single-Payload Enums

如果enum总有多个case,但是只有一个关联了data type,其他都没有,我们称这种情况为single-payload enum。此时的原则就是尽量共用空间,无法共用时,增加额外的位来区分情况。

32位足够描述所有情况,就用这么多
enum CharOrSectionMarker{//=> LLVM i32
 case Paragraph          // => i32 0x0020_0000
 case Char(UnicodeScalar)// => i32 (zext i21 %Char to i32)
 case Chapter            // => i32 0x0020_0001
}
​
CharOrSectionMarker.Char('\x00')=>i320x0000_0000
CharOrSectionMarker.Char('\u10FFFF')=>i320x0010_FFFF
​
enum CharOrSectionMarkerOrFootnoteMarker{=>LLVMi32
 case CharOrSectionMarker(CharOrSectionMarker)=>i32%CharOrSectionMarker
 case Asterisk                                =>i320x0020_0002
 case Dagger                                  =>i320x0020_0003
 case DoubleDagger                            =>i320x0020_0004
}
//不够了,总的情况为Int描述的+2,超出了Int8字节能表示的访问,就在末尾增加一个字节来区分就是是有data还是没data。
enum IntOrInfinity{=>LLVM<{i64,i1增加一位区分有没datatype}>
 case NegInfinity   =><{i64,i1}>{   0,1}
 case Int(Int)      =><{i64,i1}>{%Int,0}
 case PosInfinity   =><{i64,i1}>{   1,1}
}
​
IntOrInfinity.Int(   0)=><{i64,i1}>{    0,0}
IntOrInfinity.Int(20721)=><{i64,i1}>{20721,0}
size为9,alignment为8,strip为16

Multi-Payload Enums

如果有大于1个case关联了data type,那么就是Multi-Payload Enums。此时也是一样的尽量共用空间,无法共用时,增加bit进行区分。

class Bignum{}
​
enum IntDoubleOrBignum{=>LLVM<{i64,i2增加两位区分3中情况 }>
 case Int(Int)          =><{i64,i2}>{          %Int,           0}
 case Double(Double)    =><{i64,i2}>{(bitcast %Doubletoi64),1}
 case Bignum(Bignum)    =><{i64,i2}>{(ptrtoint%Bignumtoi64),2}
}
size为9,alignment为8,strip为16

Existential Container Layout

protocol类型、组合协议类型、Any等这些无法确定大小的类型,他们都Existential Container。具有同样的layout。

protocol

Existential Containers必须容纳任意大小和对齐的值。此时使用3个指针大小的固定数据区。如果他的大小和对其都小于等于固定缓冲区的大小,则直接包含该值。如果不能包含,这存储一个指向其数据的指针。具体是什么类型,由一个类型元数据记录标识。protocol的方法在witnesstable中。

struct OpaqueExistentialContainer{
 void*fixedSizeBuffer[3];//数据区
 Metadata*type;//数据区的类型
 WitnessTable*witnessTables[NUM_WITNESS_TABLES];//protocol的witnesstable
};
struct StructSmallP:P{
   func f(){}
   func g(){}
   func h(){}
   vara=0x6c6c616d73
   varb=0x6c6c616d73
}
letp=StructSmallP()
dumpAndOpenGraph(dumping:p,maxDepth:4,filename:"StructSmallP")
image

就两个Int的空间。ok,现在我们用Existential Container来存储他:

let p:P=StructSmallP()//定义为P
dumpAndOpenGraph(dumping:p,maxDepth:4,filename:"StructSmallP")
image
image

前两个为struct的a和b。前3个缓冲区剩余空间会用于存放valuewitnesstable中的一些方法,包含一些初始化函数,如果没空间了就没有这个。第四个为type,第5个为protocol witnesstable。正如我们最开始看到的那样:3个缓冲区,一个type,一个pwt。

AnyObject

AnyObject由于对象都是指针,所以不会存在大小不一致的情况。

let objs=[ClassA(),ClassB(),ClassC()]

Any

和protocol类似,只是没有了protocol witness table。

struct OpaqueExistentialContainer{
 void*fixedSizeBuffer[3];//数据区
 Metadata*type;//数据区的类型
};
image

推荐阅读更多精彩内容