Swift 的 MemoryLayout 是如何工作的

Easter79
• 阅读 641

前言

自从在 搜狐技术产品 公众号看过 一文看破Swift枚举本质 后,就一直计划在该文章的基础更加深入地挖掘一下Swift 枚举的内存布局。 但是,Swift 枚举的内存布局涉及的内容比较多。所以,就先把Swift 的 MemoryLayout 是如何工作的部分拆出来单独写一篇文章。

希望读者阅读本文后,能够从Swift 编译器的视角了解 MemoryLayout 是如何工作的。

本文会按照以下顺序进行讲解:

  • MemoryLayout 的 API 介绍

  • 编译器与 SIL

  • 编译器与 内置类型

  • IR

MemoryLayout

Swift 中,MemoryLayout 用于获取特定类型的内存布局信息。

Swift 的 MemoryLayout 是如何工作的

作为一个枚举,它包含3个静态变量,分别返回 size stride alignment 信息。

Review 2 SE-0101: Reconfiguring sizeof and related functions into a unified MemoryLayout struct[1] 解释了为什么是枚举而不是结构体

更多内存对齐相关知识,请参阅 size-stride-alignment[2]

struct Point {     let x: Double     let y: Double     let isFilled: Bool }

我们现在以上面的结构体 Point为例,对3个静态变量进行简单的介绍:

size

size代表 Point 类型在内存中占用的空间。

  • xDouble 类型,占用 8 byte

  • yDouble 类型,占用 8 byte

  • isFilledBool 类型,占用 1 byte

所以,MemoryLayout<Point>.size == 17

stride

stride翻译成中文是“步伐”,代表 Array<T> 中两个对象起始位置之间的距离。 为了提高性能,编译器会通过在 size 的基础上增加 7 个 byte 的方式进行内存对齐

MemoryLayout<Point>.stride == 24

alignment

同 stride 一样,为了提高性能,任何的对象都会先进行内存对齐再使用。

因为 Point 结构体中,占用空间最大的是 Double 类型。所以, MemoryLayout<Point>.alignment == 8

SIL

本文后续将会以下面的函数为目标进行分析。

func getSize() -> Int {     return MemoryLayout<Int16>.size }

该函数的实现非常简单,它会返回 Int16 类型的 size 信息。

在实际场景中,Swift 编译器会按照以下方式进行对源码进行处理。我们后续会依次介绍每个阶段。

Swift 的 MemoryLayout 是如何工作的

Parse/Sema

Parse/Sema 阶段会通过源码构建 AST,并组装类型信息。

`xcrun swiftc -dump-ast  file.swift                                                                                  (source_file "file.swift"
  (func_decl range=[file.swift:7:1 - line:9:1] "getSize()" interface type='() -> Int' access=internal
    (parameter_list range=[file.swift:7:13 - line:7:14])
    (result
      (type_ident
        (component id='Int' bind=Swift.(file).Int)))
    (brace_stmt range=[file.swift:7:23 - line:9:1]
      (return_stmt range=[file.swift:8:5 - line:8:32]
        (member_ref_expr type='Int' location=file.swift:8:32 range=[file.swift:8:12 - line:8:32] decl=Swift.(file).MemoryLayout.size [with (substitution_map generic_signature= (substitution T -> Int16))]
          (type_expr type='MemoryLayout.Type' location=file.swift:8:12 range=[file.swift:8:12 - line:8:30] typerepr='MemoryLayout'))))))

`

SILGen

SILGen 会通过 AST 信息产出以 sil_stage raw 语言版本的代码。

xcrun swiftc -emit-silgen -O file.swift | swift demangle

为了提高可读性,下面的输出都会通过 swift demangle 进行一次解析。

`sil_stage raw

import Builtin
import Swift
import SwiftShims

func getSize() -> Int

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer>>):
  %2 = integer_literal $Builtin.Int32, 0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32)        // user: %4
  return %3 : $Int32                              // id: %4
} // end sil function 'main'

// getSize()
sil hidden [ossa] @file.getSize() -> Swift.Int : $@convention(thin) () -> Int {
bb0:
  // 获得 MemoryLayout.Type 类型
  %0 = metatype $@thin MemoryLayout.Type   // user: %2

  // 获得 静态属性 size 的 get 方法
  // function_ref static MemoryLayout.size.getter
  %1 = function_ref @static Swift.MemoryLayout.size.getter : Swift.Int : $@convention(method) <τ_0_0> (@thin MemoryLayout<τ_0_0>.Type) -> Int // user: %2

  // 调用 静态属性 size 的 get 方法,参数是 MemoryLayout.Type,并返回一个 Int 类型的值
  %2 = apply %1(%0) : $@convention(method) <τ_0_0> (@thin MemoryLayout<τ_0_0>.Type) -> Int // user: %3
  // 返回结果
  return %2 : $Int                                // id: %3
} // end sil function 'file.getSize() -> Swift.Int'

//  静态属性 size 的 get 方法
// static MemoryLayout.size.getter
sil [transparent] [serialized] @static Swift.MemoryLayout.size.getter : Swift.Int : $@convention(method) <τ_0_0> (@thin MemoryLayout<τ_0_0>.Type) -> Int
`

Mandatory inlining 与 @_transparent

Guaranteed Optimization and Diagnostic Passes 是一类比较特殊的Pass。 即使开发者传入的优化命令是 none,该类优化也会被强制执行。

Mandatory inlining 就属于其中的一种。

大部分的 Swift 开发者都见过一类很特殊的函数 Transparent function

这类函数被编译时,会在 Mandatory SIL passes 阶段被强制内联处理。

MemoryLayout 源码

以本次研究的 MemoryLayout 为例, 它对应的源码如下所示:

@frozen public enum MemoryLayout<T> {   @_transparent public static var size: Swift.Int {     @_transparent get {     return Int(Builtin.sizeof(T.self))   }   }   @_transparent public static var stride: Swift.Int {     @_transparent get {     return Int(Builtin.strideof(T.self))   }   }   @_transparent public static var alignment: Swift.Int {     @_transparent get {     return Int(Builtin.alignof(T.self))   }   } }

从上面的源码,我们可以发现三个函数都被 @_transparent 修饰。

并且,size 部分的源码很简单:

  • 调用 Builtin.sizeof 获取 T.self 的大小

  • 将返回值转为 Int 类型

iOS 开发者,可以在下面的路径找 MemoryLayout 对应的源码。 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/Swift.swiftmodule/arm64.swiftinterface

Mandatory SIL passes

下面,我们看看代码经过 Mandatory inlining 处理后的情况

xcrun swiftc -emit-sil -Onone file.swift | swift demangle

通过上面的编译命令处理后,size  函数对应 SIL 如下所示:

//  静态属性 size 的 get 方法实现,对应的 Swift 版本就是  Int(Builtin.sizeof(T.self)) // static MemoryLayout.size.getter sil public_external [transparent] [serialized] @static Swift.MemoryLayout.size.getter : Swift.Int : $@convention(method) <T> (@thin MemoryLayout<T>.Type) -> Int { bb0(%0 : $@thin MemoryLayout<T>.Type):   // 获得 T.Type 类型。   %1 = metatype $@thick T.Type                    // user: %2   // 调用内置函数 sizeof 获取 T.Type 类型的 size 信息,返回结果是 Builtin.Word 类型   %2 = builtin "sizeof"<T>(%1 : $@thick T.Type) : $Builtin.Word // user: %3   // 调用 sextOrBitCast_Word_Int64 将 Builtin.Word 类型的结果转为 Builtin.Int64 类型   %3 = builtin "sextOrBitCast_Word_Int64"(%2 : $Builtin.Word) : $Builtin.Int64 // user: %4   // 将 Builtin.Int64 类型转为 Int 类型   %4 = struct $Int (%3 : $Builtin.Int64)          // user: %5   return %4 : $Int                                // id: %5 } // end sil function 'static Swift.MemoryLayout.size.getter : Swift.Int'

getSize 函数对  var size: Swift.Int 的调用也变成了 Int(Builtin.sizeof(T.self))

`sil_stage canonical

// getSize()
sil hidden @file.getSize() -> Swift.Int : $@convention(thin) () -> Int {
bb0:
  %0 = metatype $@thick Int16.Type                // user: %1
  %1 = builtin "sizeof"(%0 : $@thick Int16.Type) : $Builtin.Word // user: %2
  %2 = builtin "sextOrBitCast_Word_Int64"(%1 : $Builtin.Word) : $Builtin.Int64 // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  return %3 : $Int                                // id: %4
} // end sil function 'file.getSize() -> Swift.Int'
`

内置类型

在进一步分析 Builtin.sizeof 之前,我们先看看 Int16

Int16

MemoryLayout 类似,我们可以在 Swift.swiftmodule/arm64.swiftinterface 文件获取到 Int16 对应的源码。

@frozen public struct Int16 : Swift.FixedWidthInteger, Swift.SignedInteger, Swift._ExpressibleByBuiltinIntegerLiteral {      public var _value: Builtin.Int16  }

Swift 中,Int16 是一个结构体,它包含一个编译器内置类型 Builtin.Int16 的变量 _value

从这里开始,我们将进入 swift 编译器的世界

Builtin.Int16

对于 Builtin.Int16,Swift 编译时,会通过转为 BuiltinIntegerType 类型的实例。

Swift 的 MemoryLayout 是如何工作的

如下所示,BuiltinUnit::LookupCache::lookupValue 函数会调用 getBuiltinType 方法获取对应的类型 Type

void BuiltinUnit::LookupCache::lookupValue(        Identifier Name, NLKind LookupKind, const BuiltinUnit &M,        SmallVectorImpl<ValueDecl*> &Result) { ...    if (Type Ty = getBuiltinType(Ctx, Name.str())) {       auto *TAD = new (Ctx) TypeAliasDecl(SourceLoc(), SourceLoc(),                                           Name, SourceLoc(),                                           /*genericparams*/nullptr,                                           const_cast<BuiltinUnit*>(&M));       TAD->setUnderlyingType(Ty);       TAD->setAccess(AccessLevel::Public);       Entry = TAD; ...

getBuiltinType 会先计算出一个 BitWidth,再通过 BuiltinIntegerType::get 生成 BuiltinIntegerType 类型,并隐式转化为 Type 类型。

Type swift::getBuiltinType(ASTContext &Context, StringRef Name)  ····   // Handle 'int8' and friends.   if (Name.substr(0, 3) == "Int") {     unsigned BitWidth;     if (!Name.substr(3).getAsInteger(10, BitWidth) &&         BitWidth <= 2048 && BitWidth != 0)  // Cap to prevent insane things.       return BuiltinIntegerType::get(BitWidth, Context);   } ····

通过在lookupValue 函数的第12行添加断点,并通过 lldb 调试工具进行 dump 的结果如下所示:

(lldb) p Name (swift::Identifier) $14 = (Pointer = "Int16") (lldb) p Ty.dump() (builtin_integer_type bit_width=16) (lldb) p TAD->dump() (typealias "Int16" access=public type='Builtin.Int16')

注意:Swift编译器的源码根据 Int16 生成  BuiltinIntegerType 类型的实例,并且将 Width 设置为16

TypeInfo

Swift 的 MemoryLayout 是如何工作的

因为 Int16 是结构体,所以,编译器在产出 ir 时,会通过 swift::irgen::TypeConverter::convertStructType计算布局等信息。

考虑到 Swift结构体支持很多特殊的属性(比如 static let 计算属性 等)。所以,在构建信息前,会先筛选出能够存储值的属性(即程序运行时,需要内存空间保存属性值)。

在本例中,Int16_value属性( public var _value: Builtin.Int16)就支持存储值

`const TypeInfo *TypeConverter::convertStructType(TypeBase *key, CanType type,
                                                 StructDecl *D){

...
  // Collect all the fields from the type.
  SmallVector<VarDecl*, 8> fields;
  for (VarDecl *VD : D->getStoredProperties())
    fields.push_back(VD);

  // Build the type.
  StructTypeBuilder builder(IGM, ty, type);
  return builder.layout(fields);
}
`

准备 fields 数组后,就会调用 StructTypeBuilder 父类 RecordTypeBuilderlayout 方法进行布局

layout 方法会遍历 fields 并依次通过TypeInfo &IRGenModule::getTypeInfo(SILType T)获取TypeInfo信息,最后再拼接为 StructLayout

`TypeInfo *layout(ArrayRef astFields) {
    SmallVector<FieldImpl, 8> fields;
    SmallVector<const TypeInfo *, 8> fieldTypesForLayout;
    fields.reserve(astFields.size());
    fieldTypesForLayout.reserve(astFields.size());

    bool loadable = true;
    auto fieldsABIAccessible = FieldsAreABIAccessible;

    unsigned explosionSize = 0;
    for (unsigned i : indices(astFields)) {
      auto &astField = astFields[i];
      // Compute the field's type info.
      // 依次遍历获取 typeInfo 信息
      auto &fieldTI = IGM.getTypeInfo(asImpl()->getType(astField));
      fieldTypesForLayout.push_back(&fieldTI);

      if (!fieldTI.isABIAccessible())
        fieldsABIAccessible = FieldsAreNotABIAccessible;

      fields.push_back(FieldImpl(asImpl()->getFieldInfo(i, astField, fieldTI)));

      auto loadableFieldTI = dyn_cast(&fieldTI);
      if (!loadableFieldTI) {
        loadable = false;
        continue;
      }

      auto &fieldInfo = fields.back();
      fieldInfo.Begin = explosionSize;
      explosionSize += loadableFieldTI->getExplosionSize();
      fieldInfo.End = explosionSize;
    }

    // Perform layout and fill in the fields.
    // 产出 Int16 的 layout 信息
    StructLayout layout = asImpl()->performLayout(fieldTypesForLayout);
    for (unsigned i = 0, e = fields.size(); i != e; ++i) {
      fields[i].completeFrom(layout.getElements()[i]);
    }

    // Create the type info.
    if (loadable) {
      assert(layout.isFixedLayout());
      assert(fieldsABIAccessible);
      return asImpl()->createLoadable(fields, std::move(layout), explosionSize);
    } else if (layout.isFixedLayout()) {
      assert(fieldsABIAccessible);
      return asImpl()->createFixed(fields, std::move(layout));
    } else {
      return asImpl()->createNonFixed(fields, fieldsABIAccessible,
                                      std::move(layout));
    }
  }  
};
`

TypeInfo &IRGenModule::getTypeInfo(SILType T)最终会调用 TypeInfo *TypeConverter::getTypeEntry(CanType canonicalTy) 函数。

const TypeInfo *TypeConverter::getTypeEntry(CanType canonicalTy) {   ...   // 调用 convertType 函数,将 类型 转为 布局信息   // Convert the type.   auto *convertedTI = convertType(exemplarTy);   ...   return convertedTI; }

通过在第7行添加断点,并通过 lldb 调试工具进行 dump 的结果如下所示:

(lldb) p exemplarTy.dump() (builtin_integer_type bit_width=16) (lldb) p *convertedTI (const swift::irgen::TypeInfo) $7 = {   Bits = {     OpaqueBits = 8592015620     TypeInfo = {       Kind = 4       AlignmentShift = 1       POD = 1       BitwiseTakable = 1       SubclassKind = 7       AlwaysFixedSize = 1       ABIAccessible = 1     }     FixedTypeInfo = (Size = 2)   }   NextConverted = 0x0000000000000000   StorageType = 0x000000012f009e28   nativeReturnSchema = 0x0000000000000000   nativeParameterSchema = 0x0000000000000000 } (lldb)

通过调试工具,我们很容易注意到 FixedTypeInfo = (Size = 2)

convertPrimitiveBuiltin

实际上,对于 Builtin.Int16,编译器是通过 convertPrimitiveBuiltin 获取 size 等信息的。

LoadableTypeInfoPrimitiveTypeInfo 之间的关系比较复杂,这里不再补充类图

如下, convertPrimitiveBuiltin 方法在处理 BuiltinInteger 类型时,会通过IRGenModule::getBuiltinIntegerWidth 间接调用BuiltinIntegerType::getWidth()方法。并将 width信息转为 ByteSize信息。

`/// Convert a primitive builtin type to its LLVM type, size, and
/// alignment.
static std::tuple<llvm::Type *, Size, Alignment>
convertPrimitiveBuiltin(IRGenModule &IGM, CanType canTy) {
...
 case TypeKind::BuiltinInteger: {
    auto intTy = cast(ty);
    // 获取 bit 宽度
    unsigned BitWidth = IGM.getBuiltinIntegerWidth(intTy);
    // 转为 Byte
    unsigned ByteSize = (BitWidth+7U)/8U;
    // Round up the memory size and alignment to a power of 2.
    if (!llvm::isPowerOf2_32(ByteSize))
      ByteSize = llvm::NextPowerOf2(ByteSize);

    return RetTy{ llvm::IntegerType::get(ctx, BitWidth), Size(ByteSize),
             Alignment(ByteSize) };
  }
...
}
`

unsigned IRGenModule::getBuiltinIntegerWidth(BuiltinIntegerType *t) {   return getBuiltinIntegerWidth(t->getWidth()); }

class BuiltinIntegerType { /// Return the bit width of the integer.  Always returns a non-arbitrary   /// width.   BuiltinIntegerWidth getWidth() const {     return Width;   } }

我们在上一节的 Builtin.Int16 已经知道:BuiltinIntegerTypeWidth16。

所以,这里的结果就是 2

IR

下面,我们通过将代码转为IR的方式,验证一下上面的结论。

xcrun swiftc -emit-ir -Xfrontend -disable-llvm-optzns -Onone file.swift | swift demangle

define hidden swiftcc i64 @"file.getSize() -> Swift.Int"() #0 { entry:   ret i64 2 }

因为返回类型是Int,属于无损转换,所以编译器将所有的类型转化代码都移除了。

很明显,getSize 在产生 ir 时,就直接返回了一个i64 类型的2

结语

通过对 SIL内置类型 的分析,我们从 Swift 编译器 的视角了解 MemoryLayout 是如何工作的。

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

Swift 的 MemoryLayout 是如何工作的

参考资料

[1]

Review 2 SE-0101: Reconfiguring sizeof and related functions into a unified MemoryLayout struct: https://forums.swift.org/t/review-2-se-0101-reconfiguring-sizeof-and-related-functions-into-a-unified-memorylayout-struct/3376/5

[2]

size-stride-alignment: https://swiftunboxed.com/internals/size-stride-alignment/

本文分享自微信公众号 - 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
待兔 待兔
2个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
2年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
8个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k