[基本的なスキル]迅速なパフォーマンス最適化の詳細な分析

はじめに

2014年に、AppleはWWDC上で新しいプログラミング言語Swiftをリリースしました。 Swiftは、数年間の開発期間を経て、iOS開発言語の「主流」となっています。プロトコル、クローズ、ジェネリックなどの高度な柔軟性を備えたSwiftは、強力なSIL(Swift Intermediate Language)を開発しました。これは、SwiftがObjective-Cよりも高速に動作するように、コンパイラを最適化するために使用されます。Swiftのパフォーマンスは最適化されています。

スピーディなパフォーマンス向上の問題については、概念的には2つの部分に分割することができます。

  1. コンパイラ :Swiftコンパイラのパフォーマンス最適化は、ステージからコンパイル時および実行時まで、コンテンツは時間の最適化と領域の最適化に分かれています。
  2. 開発者 :適切なデータ構造とキーワードを使用して、コンパイラがより多くの情報を取得し、最適化できるようにします。

以下では、これらの2つの観点から、スウィフトパフォーマンスの最適化を分析します。 コンパイラのさまざまなデータ構造処理の内部実装を理解することによって、最も適切なアルゴリズムメカニズムを選択し、コンパイラの最適化機能を使用して高性能プログラムを記述します。

Swiftのパフォーマンスを理解する

Swiftのパフォーマンスを理解するには、Swiftのデータ構造、コンポーネントの関係、およびコンパイル操作を最初に理解する必要があります。

  • データ構造

    Swiftのデータ構造は、 ClassStructEnum大別できます。

  • コンポーネントの関係

    コンポーネントの関係は、 inheritanceprotocolsgenerics分類できます。

  • 発送方法

    メソッドディスパッチメソッドは、 Static dispatchDynamic dispatch分けることができます。

開発の迅速なパフォーマンスを向上させるために、開発者は、最も適切な抽象化メカニズムを選択することによって、パフォーマンスを向上させるために、これらのデータ構造とコンポーネントの関係および内部実装を理解する必要があります。

最初に、3つの基準をカバーするパフォーマンス基準の概念的なステートメントを作成します。

  • 配分
  • 参照カウント
  • メソッドディスパッチ

次に、これらの指標について個別に説明します。

配分

メモリの割り当ては、ヒープ領域のスタック領域、スタックのメモリ割り当て速度がヒープよりも大きく、構造とクラスがスタックの割り当てが異なる場合があります。

スタック

基本的なデータ型と構造体のデフォルトはスタック領域です。スタック領域のメモリは連続しており、スタックとスタックによって割り当てられ、破壊されます。速度は非常に速く、ヒープ領域よりも速いです。

いくつかの例を挙げて説明します。

//示例 1
// Allocation
// Struct
struct Point {
 var x, y:Double
 func draw() { … }
}
let point1 = Point(x:0, y:0) //进行point1初始化,开辟栈内存
var point2 = point1 //初始化point2,拷贝point1内容,开辟新内存
point2.x = 5 //对point2的操作不会影响point1
// use `point1`
// use `point2`

上記構造体のメモリはスタック領域に割り当てられ、内部変数もスタック領域にインライン展開されます。 point2からpoint2へのpoint1実際には、スタック領域にコピーを作成して新しいメモリ消費point2を作成し、 point1point1point2つのインスタンスから完全に独立させます。 point1point1を使用すると、破棄されます。

ヒープ

ヒープ領域には、クラスなどの高度なデータ構造が割り当てられています。 初期化中に未使用のメモリブロックを見つけ、メモリブロックが破棄されたときにメモリブロックから消去します。 ヒープ領域にはマルチスレッドの操作上の問題がある可能性があるため、スレッドの安全性を確保するためにはロック操作が必要で、これもパフォーマンスの低下につながります。

// Allocation
// Class
class Point {
 var x, y:Double
 func draw() { … }
}
let point1 = Point(x:0, y:0) //在堆区分配内存,栈区只是存储地址指针
let point2 = point1 //不产生新的实例,而是对point2增加对堆区内存引用的指针
point2.x = 5 //因为point1和point2是一个实例,所以point1的值也会被修改
// use `point1`
// use `point2`

上記ではClass型を初期化し、スタック領域にメモリブロックを割り当てますが、スタックに直接格納されている構造とは異なり、スタック領域にオブジェクトのポインタのみを格納し、ポインタによって指されるオブジェクトのメモリがヒープ領域に割り当てられます。 。 オブジェクトメモリを管理するためには、ヒープ領域の初期化時に属性メモリ(ここではdouble型x、y)の割り当てに加えて、 typeを含むtypeフィールドとrefCountフィールドが2つ追加されることにrefCount type refCountと実際の属性の構造はblue boxと呼ばれblue box

メモリ割り当ての概要

初期化の観点から、 Classはヒープ領域にメモリを割り当て、メモリ管理を行い、ポインタを使用し、より強力な機能を持ちますが、 Structよりもパフォーマンスが低い必要があります。

最適化方法:

頻繁な操作(通信ソフトウェアのコンテンツバブル表示など)では、 Class代わりにStructを使用してください。スタックメモリの割り当てはより高速で安全で高速です。

参照カウント

Swiftは参照カウントによってヒープオブジェクトメモリを管理します。参照カウントが0の場合、Swiftはオブジェクトがメモリを参照していないことを確認し、メモリが解放されます。 参照カウントの管理は、非常に高い頻度の間接的な操作であり、参照カウントの動作が高性能消費を必要とするように、スレッド安全性を考慮する必要がある。

基本データ型Struct場合、ヒープメモリ割り当てと参照カウントの管理はなく、パフォーマンスはより高く、より安全ですが、次のような複雑な構造の場合:

// Reference Counting
// Struct containing references
struct Label {
 var text:String
 var font:UIFont
 func draw() { … }
}
let label1 = Label(text:"Hi", font:font)  //栈区包含了存储在堆区的指针
let label2 = label1 //label2产生新的指针,和label1一样指向同样的string和font地址
// use `label1`
// use `label2`

ここでは、参照を含む構造体は、 Classと比較して参照カウントの2倍を管理しなければならないことがわかります。 構造体がパラメータとしてメソッドに渡されるか、または直接コピーされるたびに、複数の参照カウントが発生します。 次の図は、より直観的に理解することができます。

注:参照タイプを含む構造体のコピー処理

コピー中にクラスがどのように処理されるか:

参照カウントの要約

  • Classはメモリをヒープ領域に割り当て、メモリ管理のために参照カウンタを使用する必要があります。
  • Structの基本型は、参照カウント管理なしでスタック領域にメモリを割り当てます。
  • 強力に型付けされた構造体は、ポインタによってヒープのプロパティを管理します。構造体のコピーは、新しいメモリスタックを作成し、複数の参照を作成し、 Class 1つだけをClassます。

最適化方法

構造体を使用する場合:

  1. Stringの代わりにUUID(UUIDバイト長はStringの長さではなく128バイトに固定されます)などの厳密な型を使用することで、メモリインライン化を実行してスタックにUUIDを格納することができます。参照カウントは必要ありません。
  2. Enumは文字列を置き換え、スタック上のメモリを管理し、参照カウントはなく、構文的に開発者にとって使いやすいものです。

メソッドディスパッチ

静的ディスパッチVS動的ディスパッチでは、コンパイル時に実行メソッドを決定する方法を静的ディスパッチと呼びますが、コンパイル時には決定できません。実行時に実行メソッドを割り当てることのみが決定できます。派遣

Static dispatch高速で、 インライン展開などの静的ディスパッチをさらに最適化して、実行を高速化し、より優れたパフォーマンスを実現できます。

しかし、多態性については、コンパイル時に最終型を決定することはできません。ここではDynamic dispatch動的ディスパッチを使用しています。 動的ディスパッチの実装は、各型がメソッドポインタを含む配列を持つテーブルを作成することです。 動的ディスパッチはより柔軟ですが、テーブルやジャンプを検索する操作があり、多くの機能がコンパイラには明確でないため、コンパイラのポスト最適化をブロックすることと同等です。 したがって、スピードはStatic dispatchよりも遅くなります。

多態的なコードとそれを実装する方法を見てみましょう:

//引用语义实现的多态
class Drawable { func draw() {} }
class Point :Drawable {
 var x, y:Double
 override func draw() { … }
}
class Line :Drawable {
 var x1, y1, x2, y2:Double
 override func draw() { … }
}
var drawables:[Drawable]
for d in drawables {
 d.draw()
}

メソッドディスパッチの概要

Classは、コンパイル時の情報のほとんどすべてのリンクを特定できないためDynamic dispatchデフォルトでDynamic dispatch使用するため、 inlinewhole module inline inline化など、コンパイラの最適化が妨げられwhole module inline

動的ディスパッチの代わりに静的ディスパッチを使用してパフォーマンスを向上させる

Static dispatch Dynamic dispatchよりも高速で、開発中Static dispatch可能なStatic dispatchStatic dispatchを使用する方法を知っています。

  • 継承のinheritance constraints継承の制約Static dispatchを使用してFinal classを生成するために、 finalキーワードを使用してClassを修飾することができます。
  • アクセス制御
    privateキーワードは、メソッドまたはプロパティが現在のクラスでのみ表示されるように装飾されています。 コンパイラはメソッドに対してStatic dispatchを実行します。

コンパイラは、 whole module optimizationを通じて継承関係をチェックし、 finalマークしないクラスをいくつか計算します。コンパイル時に実行するメソッドを決定できる場合は、 Static dispatch使用Static dispatchます。
StructはデフォルトでStatic dispatch使用Static dispatchます。

OCより速くスウィフトするための鍵の1つは、動的ディスパッチを消散する能力です。

要約

Swiftは、メモリ、参照カウント、メソッドディスパッチなどのパフォーマンス最適化のためのより柔軟なStructを提供し、適切なデータ構造を適切なタイミングで選択することで、コードパフォーマンスをより迅速かつ安全にすることができます。

拡張する

Structがポリモーフィズムをどのように実装しているのかを尋ねるかもしれません。答えはprotocol oriented programmingです。

Protocol TypesGeneric code 、これらの3つの側面でどのように機能するのですか? Protocol TypeGeneric codeは個別にどのように実装されていますか? 私たちは、この問題をそれを見ています。

プロトコルタイプ

ここでは、プロトコルタイプが変数をどのように格納しコピーするか、メソッドディスパッチがどのように実装されるかについて説明します。 継承または参照セマンティクスのない多態性:

protocol Drawable { func draw() }
struct Point :Drawable {
 var x, y:Double
 func draw() { … }
}
struct Line :Drawable {
 var x1, y1, x2, y2:Double
 func draw() { … }
}

var drawables:[Drawable] //遵守了Drawable协议的类型集合,可能是point或者line
for d in drawables {
 d.draw()
}

上記はProtocol Typeを介した多態性であり、いくつかのクラス間の継承関係はないため、規約に従ってV-Tableによる動的ディスパッチを実装することはできません。

VtableとWitnessテーブルの実装を理解したい場合は、ここをクリックして表示します。ここには詳細はありません。
PointとLineのサイズが異なるため、配列にはExistential Containerを使用して一貫した格納用のデータが格納されます。 Protoloc Witness Tableは、正しい実行方法を見つけるために使用されます。

存在するコンテナ

Existential Containerは、同じプロトコルに準拠したデータ型Protocol Typeを管理するための特別なメモリレイアウト方法であり、同じ継承関係( V-Table実装の前提)を共有せず、メモリスペースのサイズが異なります。 Existential Containerを使用して管理し、ストレージとの整合性を確保します。

構造は次のとおりです。

  • 3ワードサイズのvalueBuffer
    valueBufferには3つの単語があり、各単語には8バイトが格納されます。格納される値は値でも、オブジェクトへのポインタでも構いません。 小さい値(スペースはvalueBufferより小さい)では、valueBufferのインラインvalueBufferのアドレスに直接格納され、追加のヒープメモリの初期化は行われません。 値の数が3つの属性、つまり大きな値、または合計サイズがvalueBufferのプレースホルダーを超えると、ヒープ領域にメモリが開かれ、ヒープ領域に格納され、valueBufferがメモリポインタを格納します。
  • Protocol Typeライフサイクルを管理するために、 Value Witness TableValue Witness Tableます。 Value Witness TableValue Witness Tableは、 Protocol Type 、メモリスペース、初期化メソッドなどが異なるために異なります。
  • プロトコル監視テーブル参照は、 Protocol Typeメソッド割り当てを管理します。

メモリの分布は次のとおりです。

1. payload_data_0 = 0x0000000000000004,
2. payload_data_1 = 0x0000000000000000,
3. payload_data_2 = 0x0000000000000000,
4. instance_type = 0x000000010d6dc408 ExistentialContainers`type    
       metadata for ExistentialContainers.Car,
5. protocol_witness_0 = 0x000000010d6dc1c0 
       ExistentialContainers protocol witness table for 
       ExistentialContainers.Car:ExistentialContainers.Drivable 
       in ExistentialContainers

プロトコル目撃者テーブル(PWT)

意味多型を参照するためにV-Tableを実装する必要がありますが、 V-Table前提は同じ親クラスが同じ継承関係を共有することですが、 Protocol Typeではこの機能は利用できません。したがって、 Structの多型性をサポートするためには、 Protocol Witness TableVtableと重要度テーブルをクリックして詳細を実装することができ、各構造体はPWTテーブルを作成し、内部にはメソッド固有のポインタを含むポインタを使用して実装されるprotocol oriented programmingメカニズムが必要です。実現する)。

価値目録テーブル(VWT)

任意の値の初期化、コピー、および破棄を管理するために使用されます。

  • Value Witness Tableは、プロトコルに準拠するProtocol Typeインスタンスの初期化、コピー、メモリの削減、および破壊を管理するために、上記のように構成されています。
  • Value Witness Tableは、 SIL %relative_vwtable%absolute_vwtableに分割することもできますが、ここでは展開しません。
  • Value Witness TableProtocol Witness Tableは、分業を通じてProtocol Typeインスタンスのメモリ管理(初期化、コピー、破棄)とメソッド呼び出しを管理します。

特定の例を詳しく見てみましょう:

// Protocol Types
// The Existential Container in action
func drawACopy(local :Drawable) {
 local.draw()
}
let val :Drawable = Point()
drawACopy(val)

Swiftコンパイラでは、 Existential Containerによって実装される疑似コードExistential Container次のとおりです。

// Protocol Types
// The Existential Container in action
func drawACopy(local :Drawable) {
 local.draw()
}
let val :Drawable = Point()
drawACopy(val)

//existential container的伪代码结构
struct ExistContDrawable {
 var valueBuffer:(Int, Int, Int)
 var vwt:ValueWitnessTable
 var pwt:DrawableProtocolWitnessTable
}

// drawACopy方法生成的伪代码
func drawACopy(val:ExistContDrawable) { //将existential container传入
 var local = ExistContDrawable()  //初始化container
 let vwt = val.vwt //获取value witness table,用于管理生命周期
 let pwt = val.pwt //获取protocol witness table,用于进行方法分派
 local.type = type 
 local.pwt = pwt
 vwt.allocateBufferAndCopyValue(&local, val)  //vwt进行生命周期管理,初始化或者拷贝
 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,这里说一下projectBuffer,因为不同类型在内存中是不同的(small value内联在栈内,large value初始化在堆内,栈持有指针),所以方法的确定也是和类型相关的,我们知道,查找方法时是通过当前对象的地址,通过一定的位移去查找方法地址。
 vwt.destructAndDeallocateBuffer(temp) //vwt进行生命周期管理,销毁内存
}

プロトコルタイプ記憶属性

Structインスタンスはスタック領域にあり、ポインタプロパティが含まれている場合はヒープ領域に格納されます。 Protocol Type属性はどのように属性に格納されますか? 小番号はExistential Containerインラインを使用して実装され、大きな番号はヒープ領域を持ちます。 コピーを扱う方法は?

プロトコル大多数のコピー最適化

コピーの場合:

let aLine = Line(1.0, 1.0, 1.0, 3.0)
let pair = Pair(aLine, aLine)
let copy = pair

新しいExsitential ContainerのvalueBufferの値は同じ値になりますが、値を変更したい場合はどうしますか? Struct値がClassとは異なって変更され、元のインスタンスの値にCopyが影響しないことがわかります。

ここでは、 Indirect Storage With Copy-On-Writeを使用したIndirect Storage With Copy-On-Writeというテクニックが使用されています。これは、メモリポインタを最初に使用します。 メモリポインタの使用を増やしてヒープメモリの初期化を減らしてください。 メモリ消費を減らす。 値を変更する必要がある場合、最初に参照カウント検出が検出され、1より大きい参照カウントがある場合は、新しいメモリが開かれ、新しいインスタンスが作成されます。 コンテンツが変更されると、新しいメモリが開きます。疑似コードは次のとおりです。

class LineStorage { var x1, y1, x2, y2:Double }
struct Line :Drawable {
 var storage :LineStorage
 init() { storage = LineStorage(Point(), Point()) }
 func draw() { … }
 mutating func move() {
   if !isUniquelyReferencedNonObjc(&storage) { //如何存在多份引用,则开启新内存,否则直接修改
     storage = LineStorage(storage)
   }
   storage。start = ...
   }
}

この実装の目的:複数のポインタを使用して同じアドレスを参照するコストは、複数のメモリヒープを開くよりもはるかに低くなります。 次の比較チャート:

プロトコルタイプポリモーフィズムの要約

  1. Protocol Type動的な多態性動作をサポートします。
  2. これは、 Witness TableExistential Containerを使用して行います。
  3. 大量のコピーは、 Indirect Storage間接記憶によって最適化することができます。

動的多型について言えば、静的多型とは何かを尋ねる必要があります。次の例を見てください。

// Drawing a copy
protocol Drawable {
 func draw()
}
func drawACopy(local :Drawable) {
 local.draw()
}

let line = Line()
drawACopy(line)
// ...
let point = Point()
drawACopy(point)

この場合、 一般 Generic code Generic codeを使用してさらに最適化を行うことができます。

ジェネリック

次に、ジェネリックプロパティの格納方法とジェネリックメソッドのディスパッチ方法について説明します。 ジェネリックスとProtocol Typeの違いは次のとおりです。

  • Genericsは静的多型をサポートしています。
  • コールコンテキストごとに1つのタイプしかありません。
    下の例を見ると、 foobarメソッドは同じ型です。
  • 型の置き換えは、コールチェーン内の型のダウングレードによって実行されます。

次の例について:

func foo<T:Drawable>(local :T) {
 bar(local)
}
func bar<T:Drawable>(local:T) { … }
let point = Point()
foo(point)

メソッドfoobarの呼び出しプロセスを分析する:

//调用过程
foo(point)->foo<T = Point>(point)   //在方法执行时,Swift将泛型T绑定为调用方使用的具体类型,这里为Point
 bar(local) ->bar<T = Point>(local) //在调用内部bar方法时,会使用foo已经绑定的变量类型Point,可以看到,泛型T在这里已经被降级,通过类型Point进行取代

汎用メソッド呼び出しの具体的な実装は次のとおりです。

  • 同じタイプのインスタンスは、同じプロトコル証人テーブルを使用して同じ実装を共有します。
  • プロトコル/値監視テーブルを使用します。
  • コールコンテキストごとに1つのタイプしかありません。 Existential Containerを使用する代わりに、追加パラメータとしてProtocol/Value Witness Tableが呼び出し元に渡されます。
  • 変数の初期化とメソッドの呼び出しは、渡されたVWTPWTを使用して実行されます。

ここでは、ジェネリックスがProtocol Typeより高速な機能を備えているとは考えていませんが、ジェネリックスの高速化は可能ですか?

一般的な専門分野

  • 静的多型:呼び出し元ステーションには1つの型しかありません
    Swiftは、タイプダウングレード置換を実行するために、1つのタイプの機能のみを使用します。
  • タイプがダウングレードされた後に特定のタイプのメソッドを生成する
  • ジェネリックの各タイプに対応するメソッドを作成します。この時点で、それぞれのタイプが新しいメソッドを生成し、コードスペースが爆発しないことを尋ねるかもしれません。
  • 静的多型下での最適化の specialization
    それは静的多型ですから。 したがって、インライン実装などの非常に強力な最適化や、コンテキストの取得による最適化を行うことができます。 これにより、方法の数が減少する。 最適化されたものは、より正確で具体的なものになります。

例えば:

func min<T:Comparable>(x:T, y:T) -> T {
  return y < x ? y : x
}

すべてのタイプのminメソッドがサポートされているため、初期化アドレス、メモリ割り当て、ライフサイクル管理などの汎用タイプの計算が必要になるため、一般ジェネリックスは次のように拡張されます。 値の操作に加えて、メソッドも操作されます。 これは非常に複雑で巨大なプロジェクトです。

func min<T:Comparable>(x:T, y:T, FTable:FunctionTable) -> T {
  let xCopy = FTable.copy(x)
  let yCopy = FTable.copy(y)
  let m = FTable.lessThan(yCopy, xCopy) ? y :x
  FTable.release(x)
  FTable.release(y)
  return m
}

Intなどの入力タイプを決定するとき、コンパイラは汎用の特殊化によってタイプ置換(タイプ置換)を実行し、次のように最適化されます。

func min<Int>(x:Int, y:Int) -> Int {
  return y < x ? y :x
}

ジェネリック specilizationはいつ行われましたか?

特定の最適化を使用する場合、呼び出し側はタイプの定義や内部メソッドの実装など、タイプのコンテキストを知る必要があるタイプの推論を行う必要があります。 呼び出し元と型が別々にコンパイルされている場合は、呼び出し元によって内部的に推論することはできず、特定の最適化を使用してコードを一緒にコンパイルすることはできません。 whole module optimizationは、呼び出し元と呼び出し先のメソッドが異なるファイルにある場合のメソッドの一般的な最適化の前提条件です。

一般的な最適化

特定のジェネリック医薬品のさらなる最適化:

// Pairs in our program using generic types
struct Pair<T :Drawable> {
 init(_ f:T, _ s:T) {
 first = f ; second = s
 }
 var first:T
 var second:T
}
let pairOfLines = Pair(Line(), Line())
// ...

let pairOfPoint = Pair(Point(), Point())

対ジェネリックの使用は複数のジェネリックが使用され、実行時にジェネリック型が変更されないと判断された場合に、さらに最適化することができます

最適化方法は、ポインタからメモリインラインへの汎用メモリ割り当てを割り当て、追加のヒープ初期化消費はありません。 記憶域のインライン化のために、一般的な種類のメモリ分布が決定され、汎用メモリのインライン化では異なる種類の記憶ができないことに注意してください。 したがって、 この最適化は、実行時ジェネリック型が変更されない 、つまりあるメソッドでlinepoint両方の型をサポートできないという事実にのみ適用されます

全モジュール最適化

whole module optimizationは、Swiftコンパイラの最適化メカニズムです。 -whole-module-optimization (または-wmo )で-whole-module-optimizationことができます。 XCode 8の後では、デフォルトでオンになっています。 Swift Package Managerwhole module optimizationデフォルトでリリースモードでwhole module optimization使用します。 モジュールは、複数のファイルの集合です。

コンパイラは、ソースファイルを解析した後、最適化し、マシンコードを生成し、オブジェクトファイルを出力します。リンカはすべてのターゲットファイルを結合して、共有ライブラリまたは実行可能ファイルを生成します。
whole module optimizationは、型の具体的な実装を取得し、型劣化メソッドの逆変換を実行し、重複したメソッドを削除することで、推論の最適化を実行できます。

フルモジュール最適化の利点

  • コンパイラは、すべてのメソッドの実装をマスターし、 インライン化汎用の特殊化などの最適化を実行し、すべてのメソッドへの参照を計算して冗長参照カウント演算を削除します。
  • すべての非公開メソッドを知ることによって、このメソッドを使用しない場合は、それを排除できます。

コンパイル時間を短縮する方法 <br />モジュール全体の最適化とは逆に、ファイルの最適化は1つのファイルをコンパイルすることです。 これは、並列に実行できるという利点があり、変更されていないファイルに対しては再度コンパイルされません。 欠点は、コンパイラが画像全体を把握せず、深い最適化を行うことができないことです。 以下では、フルモジュール最適化が、コンパイルされていないファイルを再度コンパイルするのを避ける方法を分析します。

コンパイラの内部実行プロセスは、構文解析、型チェック、 SIL最適化、 LLVMバックエンド処理に分かれています。

構文解析と型チェックは一般的に高速で、 SIL最適化は、総コンパイル時間の約3分の1を占める一般的な特殊化やメソッドのインライン化など、重要なSwift固有の最適化を実行します。 LLVMバックエンドの実行は、ダウングレードの最適化とコードの生成を実行するためのコンパイル時間の大部分を占めます。

フルモジュールの最適化後、 SIL最適化により、モジュールが複数の部分に分割されますLLVMバックエンドは、分割されたモジュールをマルチスレッドで処理し、変更されていない部分は再処理しません。 これにより、マルチスレッド並列処理を使用して処理時間を短縮することに加えて、 LLVMバックエンドの再実行を実行するために、大規模モジュール全体の小さな部分を変更することが回避されます。

拡張子:Swiftの隠された "Bug"

Swiftにはメソッドのディスパッチに関する問題があります。設計と最適化の後では、従来の理解と一致しない結果が生成されますが、これはバグではありません。 しかし、メカニズムを熟知していないために、開発プロセスの問題を避けるために、別々に説明する必要があり、結果として期待と実装につながります。

メッセージディスパッチ

前述のように、メソッド代入を静的ディスパッチVS動的ディスパッチと組み合わせる方法について理解しています。 ここでは、 Objective-Cメソッドのディスパッチの方法について説明する必要があります。

OCに精通している人なら、OCはランタイムメカニズムを使用してobj_msgSendを使ってメッセージを送信することをobj_msgSendますが、実行時には非常に柔軟性があります。フックと有名なKVO

スウィフトを使用する開発の誰もが頼みます、スウィフトは、OCのランタイムとメッセージ転送のメカニズムを使用することができますか? 答えは「はい」です。

Swiftはキーワードdynamic使用してメソッドをマークできます。このキーワードは、このメソッドがOCの実行時メカニズムを使用することをコンパイラに通知します。

注意:共通キーワード@ObjCは、Swiftの元のメソッドディスパッチメカニズムを変更しません。キーワード@ObjCの役割は、コードがOCに可視であることをコンパイラに伝えることです。

要約すると、Swiftにはdynamicキーワードの拡張によるStatic dispatchTable dispatchMessage dispatch 3つのディスパッチ方法があります。 次の表は、さまざまな状況で異なるデータ構造がどのように割り当てられるかを示しています。

Swift dispatch method

開発プロセスに間違いがあると、これらのバグが混在する可能性があります。以下のバグを分析します。

SR-584
この場合、親クラスのメソッドがサブクラスの拡張でオーバーライドされると、予想と異なる動作が発生します。

class Base:NSObject {
    var directProperty:String { return "This is Base" }
    var indirectProperty:String { return directProperty }
}

class Sub:Base { }

extension Sub {
    override var directProperty:String { return "This is Sub" }
}

問題なく直接呼び出し、次のコードを実行します。

Base().directProperty // “This is Base”
Sub().directProperty // “This is Sub”

間接呼び出しの結果は期待と異なります。

Base()。indirectProperty // “This is Base”
Sub()。indirectProperty // expected "this is Sub",but is “This is Base” <- Unexpected!

Base.directProperty前にdynamicキーワードを追加することで、 "this is Sub"の結果を得ることができます。 拡張文書内の既存のメソッドを上書きすることはできません。

拡張機能はあるタイプに新しい機能を追加することができますが、既存の機能を上書きすることはできません。

警告: Cannot override a non-dynamic class declaration from an extension

この問題の理由は、NSObject拡張がMessage dispatchであり、 Initial DeclarationTable dispath使用するためTable dispath (前述のSwift Dispatchメソッドを参照)。 仮想関数テーブルは、依然として親クラスメソッドであるため、親クラスメソッドが実行されます。 拡張機能でメソッドをオーバーロードするには、 Message dispatchを使用するためにdynamicを指定する必要があります。

SR-103

プロトコルの拡張の中で実装されたメソッドは、そのクラスに準拠するサブクラスによってオーバーライドできません:

protocol Greetable {
    func sayHi()
}
extension Greetable {
    func sayHi() {
        print("Hello")
    }
}
func greetings(greeter:Greetable) {
    greeter.sayHi()
}

次に、プロトコルPerson続くクラスを定義します。 プロトコルクラスLoudPersonサブクラスに従ってください:

class Person:Greetable {
}
class LoudPerson:Person {
    func sayHi() {
        print("sub")
    }
}

次のコード結果を実行します。

var sub:LoudPerson = LoudPerson()
sub.sayHi()  //sub

期待を満たしていないコード:

var sub:Person = LoudPerson()
sub.sayHi()  //HellO  <-使用了protocol的默认实现

overrideキーワードはサブクラスLoudPersonは表示されません。 LoudPersonWitness table Greetableのメソッドを正常に登録していないことがわかりWitness table 。 したがって、実際にPerson LoudPersonとして宣言されたインスタンスの場合、コンパイラでPersonを介してPersonがプロトコルメソッドを実装していないことが判明し、次にWitness tableメソッドが直接呼び出されます。 解決策は、基本クラスにプロトコルメソッドを実装し、実装せずにデフォルトメソッドを提供することです。 継承を避けるために、基本クラスをfinalとしてマークします。

さらに例を理解する:

// Defined protocol。
protocol A {
    func a() -> Int
}
extension A {
    func a() -> Int {
        return 0
    }
}

// A class doesn't have implement of the function。
class B:A {}

class C:B {
    func a() -> Int {
        return 1
    }
}

// A class has implement of the function。
class D:A {
    func a() -> Int {
        return 1
    }
}

class E:D {
    override func a() -> Int {
        return 2
    }
}

// Failure cases。
B().a() // 0
C().a() // 1
(C() as A).a() // 0 # We thought return 1。 

// Success cases。
D().a() // 1
(D() as A).a() // 1
E().a() // 2
(E() as A).a() // 2

その他

私たちはクラス拡張がStatic Dispatchを使用することを知っています:

class MyClass {
}
extension MyClass {
    func extensionMethod() {}
}
 
class SubClass:MyClass {
    override func extensionMethod() {}
}

上記のコードはDeclarations in extensions can not be overridden yetDeclarations in extensions can not be overridden yet示すエラーが発生します。

要約

  • プログラムに影響を及ぼす3つのパフォーマンス基準、すなわち初期化メソッド参照ポインター、およびメソッド割り当てがあります。
  • この論文では、2つのデータ構造を比較しています。 Swiftは、OCや他の言語と比較して構造体の機能を向上させるので、上記のパフォーマンスを理解しながら構造体を活用することで、パフォーマンスを向上させることができます。
  • これに基づいて、強力な構造のクラスを導入しました: Protocol TypeGeneric 。 また、多態性をどのようにサポートしているか、条件付きの制限を使用してプログラムを高速化する方法についても説明します。

参照資料

著者紹介

アジアの男性、米国のグループがiOSエンジニアにコメントしました。 2017年、彼は米国のグループレビューに加わり、ケータリングバトラーの専門版の開発とコンパイラの原則の研究を担当しました。 現在、Swiftコンポーネント化が積極的に推進されています。

採用情報

私たちのケータリングエコロジーテクノロジー部門は、技術的雰囲気が活発で大きな牛が集まる場所です。 新しい到着店は、大規模なSaaS、マルチテナント、データ、セキュリティ、オープンプラットフォームおよびその他の課題に対する現実の機会を把握しています。 ビジネス分野には複雑な技術的課題、技術とビジネス能力の急速な進歩、そして最も重要なことに、私たちに加わることで、真にコードを通じて業界を変える夢を実現します。 我々は参加するすべての才能を歓迎し、Javaが好ましい。 興味のある生徒はすぐに履歴書を[email protected]に送ります。到着をお待ちしております。

元のリンク