[Swift]動的ディスパッチを減らすことでパフォーマンスを改善
スマホデバイスのスペックも向上し、Swiftのコンパイラのパフォーマンスも上がり続けていますが、ちょっとしたパフォーマンスが気になることはよくあります。
スマホアプリではパフォーマンス次第では使い勝手が極端に悪くなります。
finalやprivateを駆使したパフォーマンス改善はJava等の多言語でもおなじみで応用が効くものです。無闇にfinalやpriavteをつけている場合も多いでしょうが、パフォーマンス改善の理屈が分かると上手に使用できるようになると思います。
全モジュール最適化(Whole Module Optimization)は開発時は不要ですが、リリース時には必ず実行しておきたいですね。
動的ディスパッチを減らすことでパフォーマンスを改善
他の多くの言語と同じく、Swiftでも親クラスで定義されたメソッドやプロパティをクラス内でオーバーライドすることができます。このため、プラグラムはどのメソッドやプロパティが参照されているかを実行時に決定してから、間接的にメッソドを呼び出したりプロパティにアクセスしたりしなければなりません。これは動的ディスパッチという技術で、間接的に使用されることによる実行時のオーバーヘッドをある程度犠牲にして、言語としての表現度を高めてくれます。しかしパフォーマンスが気にかかるコードではそのようなオーバーヘッドは望ましくありません。この記事では動的な処理を取り除いてパフォーマンスを向上させる3つの手段(final、private、全モジュール最適化)を紹介します。
次の例を考えてみましょう。
class ParticleModel {
var point = ( 0.0, 0.0 )
var velocity = 100.0
func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
p.update((i * sin(i), i), newV:i*1000)
}
上記では、コンパイラは動的ディスパッチによる呼び出しを以下の順で行います。
- pインスタンスのupdateメソッドを呼び出す
- pインスタンスのupdatePointメソッドを呼び出す
- pインスタンスのプロパティpointにアクセスする
- pインスタンスのプロパティvelocityにアクセスする
このコードでは動的な呼び出しは期待していないかもしれません。PartialModelのサブクラスでは計算されたプロパティでpointやvelocityをオーバーライドするかもしれないし、別の実装でupdatePoint()やupdate()をオーバーライドするかもしれませんので、動的な呼び出しは必要なのです。
Swiftでは、動的ディスパッチ呼び出しはメソッドテーブルから関数を探しだし、それから間接的な呼び出しを実行するように実装されています。このために直接的な呼び出しと比較してパフォーマンスが劣るのです。さらに、間接的な呼び出しはコンパイラによる最適化の多くにとって邪魔であり、さらにパフォーマンスが悪くなる原因となっています。パフォーマンスが厳しいコードでは、動的な処理を必要としていないときにはパフォーマンス向上のために、それらを制限する手段があります。
定義がオーバーライドされる必要がないときはfinalを使う
finalという予約後はクラス、メソッド、プロパティに定義がオーバーライドされないように制限を加えるものです。これにより、コンパイラはダイナミックディスパッチによる間接的な呼び出しを安全に省力することができます。例えば、以下のpointとvelocityはロード時にオブジェクトに格納されたプロパティから直接アクセスすることができます。updatePoint()は直接関数を呼び出すことができます。一方、update()は動的ディスパッチ経由のままで、サブクラスで機能をカスタマイズしてオーバーライドすることができます。
class ParticleModel {
final var point = ( x: 0.0, y: 0.0 )
final var velocity = 100.0
final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
クラス自身に属性をつけることで、クラス全体をfinalにすることもできます。サブクラスをつくることを禁じて、クラスの関数やプロパティすべてをfinalとして扱います。
final class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
// ...
}
予約後privateを適用することでファイル内で参照されている宣言からfinalを推定する
宣言に予約後privateをつけることで、その宣言の可視範囲を現在のファイル内に制限します。こうすることでコンパイラはオーバーライドしている可能性のある宣言をすべて見つけることができます。宣言がオーバーライドされていないとわかると、コンパイラは自動的にfinal予約語がついていると推定してメソッド呼び出しやプロパティアクセスを間接的に行なうのをやめます。
現在のファイル内にはParticleModelをオーバーライドしているクラスはないと仮定すると、コンパイラはprivateな宣言へのすべての動的ディスパッチによる呼び出しを直接呼び出しに変更します。
class ParticleModel {
private var point = ( x: 0.0, y: 0.0 )
private var velocity = 100.0
private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
この例では、pointとvelocityには直接アクセスし、updatePoint()を直接呼び出すことができます。update()はprivateではないので間接的に呼び出されます。
finalと同様、private属性をクラスの宣言自身につけることができます。こうすることでクラスとクラス内のすべてのプロパティとメソッドはprivateとして扱われます。
private class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
// ...
}
internal宣言でfinalを推定するモジュール全体最適化(Whole Module Optimization)を使用する
internalな宣言(デフォルでは宣言に何もついていないとそうなります)は宣言されたモジュール内でのみ可視されます。コンパイラは通常はモジュールを分割して記述してあるファイルごとにコンパイルするので、別のファイル内でinternalな宣言がオーバーライドされているかどうかを判断することができません。しかし、モジュール全体最適化を有効にすると、全モジュールを一度にコンパイルします。これによりコンパイラはモジュール全体を通しての推定を行なうことができるようになり、internalな宣言の可視範囲内にオーバーライドがなければその宣言をfinalと推定します。
最初のコードに戻って、ParticleModelに予約後publicをつけてみましょう。
public class ParticleModel {
var point = ( x: 0.0, y: 0.0 )
var velocity = 100.0
func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
point = newPoint
velocity = newVelocity
}
public func update(newP: (Double, Double), newV: Double) {
updatePoint(newP, newVelocity: newV)
}
}
var p = ParticleModel()
for i in stride(from: 0.0, through: times, by: 1.0) {
p.update((i * sin(i), i), newV:i*1000)
}
このコードをモジュール全体最適化(Whole Module Optimization)でコンパイルすると、コンパイラはプロパティのpoint、velocityとメソッド呼び出しのupdatePoint()をfinalと推定します。対照的に、update()はpublicアクセスを持っているので、finalと推定することはできません。