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指向的实例对象的内存模式大概长什么样呢?
现在还看不懂,我们先介绍下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
再看对象内存布局
再看看一开始的图:
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个无主引用:
可以看到06(00000110),其中11位无主引用数3,0a(00001010),其中101为额外强引用计数。
ok,我们在试试定义一个weak,让对象有sidetable(因为weak引用计数在sidetable的SideTableRefCountBits中)。
weak var p8=p1
可以看到ref已经变味一个指针+flag。
Class的layout
这是Person这个Class中的信息,读取的信息描述也不是很清晰,我们大概可以认为他是一个特殊的vtable,混杂了一些oc类结构体中的一些信息。
Class{
isa
supperclass
cache
各种method指针
}
这是放大的部分,128字节开始是sayHello函数,136字节开始是sayWorld函数,还有各种getter和setter等。
第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")
就两个Int的空间。ok,现在我们用Existential Container来存储他:
let p:P=StructSmallP()//定义为P
dumpAndOpenGraph(dumping:p,maxDepth:4,filename:"StructSmallP")
前两个为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;//数据区的类型
};