はじめに
新社会人2ヶ月目の新米エンジニアです。
Swiftのclass funcとstatic funcの違いについて簡単に調べてみました。
(2018/06/03 finalによる速度の違いであることが判明しましたので,追記させていただきました。)
機能差
class func はclass内でしか呼び出せない一方で,override が使用可能です。
static func は override ができない一方で,struct や enum でも使用可能です。
class | static | |
---|---|---|
override | 可能 | 不可能 |
class内利用 | 可能 | 可能 |
struct内利用 | 不可能 | 可能 |
enum内利用 | 不可能 | 可能 |
class func == final static func ?
色々と調べていたところ,
What is the difference between static func and class func in Swift? に
static func is same as final class func
という記述,また,
Swiftのclass funcとstatic funcの違い にも,
static func は final class func と同義
といった記述を見つけました。
これはコードレベルで同一のものを吐き出すのでしょうか?
準備
version
$ swiftc -v
Apple Swift version 4.1 (swiftlang-902.0.48 clang-902.0.37.1)
Target: x86_64-apple-darwin17.5.0
class func v. static func
class funcとstatic funcが共に使用可能なclassでの比較を行った。
比較対象は普段のコーディングを想定し, class func と static func とする。
各Main classに"Hello, world!!"を出力するfunctionを用意し,swiftcでアセンブリを生成する。
コンパイルオプションはデフォルトを指定(swiftc -emit-bc -O
やllc -O3
は無意味であった)。
class Main {
class func say() {
print("Hello, world!!")
}
}
Main.say()
class Main {
static func say() {
print("Hello, world!!")
}
}
Main.say()
SIL比較
silの生成
$ swiftc -emit-silgen class-func.swift > class-func.sil
$ swiftc -emit-silgen static-func.swift > static-func.sil
確認
diff class-func.sil static-func.sil
@@ -7,8 +7,9 @@ import SwiftShims
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
- %2 = metatype $@thick Main.Type // users: %4, %3
- %3 = class_method %2 : $@thick Main.Type, #Main.say!1 : (Main.Type) -> () -> (), $@convention(method) (@thick Main.Type) -> () // user: %4
+ %2 = metatype $@thick Main.Type // user: %4
+ // function_ref static Main.say()
+ %3 = function_ref @_T04main4MainC3sayyyFZ : $@convention(method) (@thick Main.Type) -> () // user: %4
%4 = apply %3(%2) : $@convention(method) (@thick Main.Type) -> ()
%5 = integer_literal $Builtin.Int32, 0 // user: %6
%6 = struct $Int32 (%5 : $Builtin.Int32) // user: %7
@@ -117,7 +118,6 @@ bb0(%0 : $Main):
} // end sil function '_T04main4MainCACycfc'
sil_vtable Main {
- #Main.say!1: (Main.Type) -> () -> () : _T04main4MainC3sayyyFZ // static Main.say()
#Main.init!initializer.1: (Main.Type) -> () -> Main : _T04main4MainCACycfc // Main.init()
#Main.deinit!deallocator: _T04main4MainCfD // Main.__deallocating_deinit
}
class funcはMain.sayをsil_vtableで宣言し,動的ディスパッチをしている。
static funcはgeneric functionから直接呼び出しをしている。
アクセス方法が異なるだけで,class funcも
// static Main.say()
のコメントとともに,同じ関数が用意されている。
アセンブリ比較
assemblyの生成
$ swiftc -emit-assembly class-func.swift > class-func.s
$ swiftc -emit-assembly static-func.swift > static-func.s
確認
diff class-func.s static-func.s
@@ -385,7 +385,7 @@ l___unnamed_4:
__T04main4MainCMn:
.long l___unnamed_3-__T04main4MainCMn
.long 0
- .long 12
+ .long 11
.long (l___unnamed_4-__T04main4MainCMn)-12
.long (l_get_field_types_Main-__T04main4MainCMn)-16
.long 0
@@ -397,10 +397,8 @@ __T04main4MainCMn:
.short 4
.long 0
.long 10
- .long 2
- .long (__T04main4MainC3sayyyFZ-__T04main4MainCMn)-56
- .long 0
- .long (__T04main4MainCACycfc-__T04main4MainCMn)-64
+ .long 1
+ .long (__T04main4MainCACycfc-__T04main4MainCMn)-56
.long 1
.zerofill __DATA,__bss,__T04main4MainCML,8,3
@@ -419,11 +417,10 @@ __T04main4MainCMf:
.long 16
.short 7
.short 0
- .long 112
+ .long 104
.long 16
.quad __T04main4MainCMn
.quad 0
- .quad __T04main4MainC3sayyyFZ
.quad __T04main4MainCACycfc
.section __TEXT,__swift3_typeref,regular,no_dead_strip
出力されたアセンブリを見てみると,
static func が1行分だけ処理が短くなっている。
final class func v. static func
次に,final class funcとstatic funcが共に使用可能なclassでの比較を行った。
比較対象は final class func と static func とする。
各Main classに"Hello, world!!"を出力するfunctionを用意し,swiftcでアセンブリを生成する。
コンパイルオプションはデフォルトを指定(swiftc -emit-bc -O
やllc -O3
は無意味であった)。
class Main {
final class func say() {
print("Hello, world!!")
}
}
Main.say()
class Main {
static func say() {
print("Hello, world!!")
}
}
Main.say()
SIL比較
silの生成
$ swiftc -emit-silgen final-class-func.swift > final-class-func.sil
$ swiftc -emit-silgen static-func.swift > static-func.sil
確認
SILレベルで全く同じファイルが生成された。(当たり前だが)そこから生成されるアセンブリも同一のファイルを生成した。
まとめ
overrideができない点で安全なのはstatic func。
enum等にも利用できるのもstatic func。
protocol指向にもstatic func
じゃけんstatic funcに移行しましょうね~。
...という簡単な話ではなかった。
final class func は static funcと完全に等価であり,置換しても問題ありません。
class func は override を利用していないのであればfinalを付ける,またはstatic funcに置き換えることで
パフォーマンスがアセンブリ1行レベルで向上します。
個人的にはfinalの有無は最適化(swiftc -O llc -O3)をすり抜けて挙動に影響を与えると知ることができたのも大きい。
2018/06/03 追記
class func は override を利用していないのであればfinalを付ける,またはstatic funcに置き換えることで
パフォーマンスがアセンブリ1行レベルで向上します。
という話でもなく,
個人的にはfinalの有無は最適化(swiftc -O llc -O3)をすり抜けて挙動に影響を与えると知ることができたのも大きい。
...という簡単な話でもありませんでした。
[Swift]動的ディスパッチを減らすことでパフォーマンスを改善 を読んで,
この問題はstaticとclassの問題ではないことが分かりました。
finalを使用することで,コンパイラによって動的ディスパッチが直接呼び出しになるため,処理速度が向上する,ということでした。
class func v. final class func
と,いうことは,アクセス修飾子によってコンパイラが結果的にfinalと予測可能なように宣言すれば,finalもstaticも付けずともパフォーマンスの高いコードを生成することが可能ということが予測できます。
private キーワードも比較するため,init()にて呼び出すように変更します。
class Main {
init() {
Main.say()
}
<foo> class func say() {
print("Hello, world!!")
}
}
let _ = Main()
上記コードの <foo> の部分を様々なアクセス修飾子(private ... public)に変更してみます。
SIL比較
finalの有無によって変更されるため,SILのみで比較を行った。
internalのfinalに影響する最適化オプションが指定できたため,Release mode
を追加した。
swiftc -emit-silgen -wmo -O final-class-func.swift > ./sil/final-class-func.sil
swiftc -emit-silgen -wmo -O static-func.swift > ./sil/static-func.sil
swiftc -emit-silgen -wmo -O open-class-func.swift > ./sil/open-class-func.sil
swiftc -emit-silgen -wmo -O public-class-func.swift > ./sil/public-class-func.sil
swiftc -emit-silgen -wmo -O internal-class-func.swift > ./sil/internal-class-func.sil
swiftc -emit-silgen -wmo -O fileprivate-class-func.swift > ./sil/fileprivate-class-func.sil
swiftc -emit-silgen -wmo -O private-class-func.swift > ./sil/private-class-func.sil
diff ./sil/final-class-func.sil ./sil/static-func.sil
diff ./sil/final-class-func.sil ./sil/open-class-func.sil
diff ./sil/final-class-func.sil ./sil/public-class-func.sil
diff ./sil/final-class-func.sil ./sil/internal-class-func.sil
diff ./sil/final-class-func.sil ./sil/fileprivate-class-func.sil
diff ./sil/final-class-func.sil ./sil/private-class-func.sil
diffで比較しても良いですが,
今回は出力されるファイル内容が分かっているため,.silファイルの43行目がfunction_ref
かclass_method
かにより判別できます。
sed -n 43p ./sil/final-class-func.sil
sed -n 43p ./sil/static-func.sil
sed -n 43p ./sil/open-class-func.sil
sed -n 43p ./sil/public-class-func.sil
sed -n 43p ./sil/internal-class-func.sil
sed -n 43p ./sil/fileprivate-class-func.sil
sed -n 43p ./sil/private-class-func.sil
結果
アクセス修飾子(+static) | finalの生成 |
---|---|
static func | ○ |
open class func | × |
public class func | × |
internal class func | × |
fileprivate class func | × |
private class func | × |
変換してくれていない。swiftcのみでは最適化してくれていない可能性が浮上した。
逆アセンブル
internalのfinalに影響する最適化オプションが指定できたため,Release mode
を追加した。
少し確認したところ, Swift Compiler Performance によると
どうやら,Release modeのビルドはXcodebuild
を用いて行う様子。
また,Swift3よりデフォルトのアクセス修飾子にopen
が追加され,モジュール外からのoverrideは明示的に行う必要がでてきました。
と,いうことは,Release modeにてBuildを実施した場合,コンパイラはopen
以外の全てをfinalと予測することができると予想できます。
xcrun swiftc -wmo -O ./final-class-func.swift
xcrun swiftc -wmo -O ./static-func.swift
xcrun swiftc -wmo -O ./open-func.swift
xcrun swiftc -wmo -O ./public-class-func.swift
xcrun swiftc -wmo -O ./internal-class-func.swift
xcrun swiftc -wmo -O ./fileprivate-class-func.swift
xcrun swiftc -wmo -O ./private-class-func.swift
Swift実行ファイルを逆アセンブルして,最適化具合を正確に把握する方法 を参考に,Hopper 4を利用して逆アセンブルを行いました。
結果
アクセス修飾子(+static) | finalの生成 |
---|---|
static func | ○ |
open class func | ○ |
public class func | ○ |
internal class func | ○ |
fileprivate class func | ○ |
private class func | ○ |
先程とは相反する結果となりました。
この結果から言えることは,コンパイラが優秀(または,私の検証方法が間違っている)ということ。特にopenの挙動が不穏です。
まとめ
static func と class func の違いを語る上で,以下4つの点に注意する必要があります。
1点目,static func は final class func と完全に等価であるということ。
2点目,final キーワードを付けることで呼び出しが動的ディスパッチから直接呼び出しになり,パフォーマンスが向上するということ。
3点目,privateなどのアクセス修飾子によって範囲を制限することで,コンパイラがfinalと推測して直接呼び出しに変換してくれるということ。
4点目,XcodebuildでRelease buildを行うと,コンパイラが最適化してくれるということ。
コンパイル方法によって異なったコードが吐き出されることには少し違和感を覚えました。
しかし,これが事実であればコンパイラは差分を吸収し,コーダーはあれこれ悩んでコーディングをする必要がなくなった,と言うことができます。
検証方法に誤りがある場合には是非コメント等で教えていただけますと幸いです。
参考
What is the difference between static func and class func in Swift?
Swiftのclass funcとstatic funcの違い
【Swift】classとstaticの挙動の違いを整理する
Merge pull request #15151 from ikesyo/stdlib-public-operator-static-func
Swift 関数の再帰呼び出しは最適化されているか LLVM
Swift Intermediate Language (SIL)
[Swift]動的ディスパッチを減らすことでパフォーマンスを改善
class func vs static func – Reddit
Taming Swift compiler bugs
Swift実行ファイルを逆アセンブルして,最適化具合を正確に把握する方法
Swiftのfinalについて
Swiftのfinal・private・Whole Module Optimizationを理解しDynamic Dispatchを減らして,パフォーマンスを向上する
Increasing Performance by Reducing Dynamic Dispatch
Writing High-Performance Swift Code
Swift Compiler Performance
vtableの中身を見てみる
仮想関数テーブル
LLVM Programmer’s Manual
ビューティフルアーキテクチャ
Rubyでのメタプログラミング(動的ディスパッチと動的メソッド)