LoginSignup
58
33

More than 5 years have passed since last update.

class func v. static func in Swift4

Last updated at Posted at 2018-05-25

はじめに

新社会人2ヶ月目の新米エンジニアです。

Swiftのclass funcとstatic funcの違いについて簡単に調べてみました。

(2018/06/03 finalによる速度の違いであることが判明しましたので,追記させていただきました。)

機能差

class func はclass内でしか呼び出せない一方で,override が使用可能です。
static funcoverride ができない一方で,structenum でも使用可能です。

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 funcstatic func とする。
各Main classに"Hello, world!!"を出力するfunctionを用意し,swiftcでアセンブリを生成する。
コンパイルオプションはデフォルトを指定(swiftc -emit-bc -Ollc -O3は無意味であった)。

class-func.swift
class Main {
    class func say() {
        print("Hello, world!!")
    }
}
Main.say()
static-func.swift
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 funcstatic func とする。
各Main classに"Hello, world!!"を出力するfunctionを用意し,swiftcでアセンブリを生成する。
コンパイルオプションはデフォルトを指定(swiftc -emit-bc -Ollc -O3は無意味であった)。

final-class-func.swift
class Main {
    final class func say() {
        print("Hello, world!!")
    }
}
Main.say()
static-func.swift
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 funcstatic 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を追加した。

generate.sh
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.sh
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_refclass_methodかにより判別できます。

sed.sh
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 funcclass func の違いを語る上で,以下4つの点に注意する必要があります。

1点目,static funcfinal 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でのメタプログラミング(動的ディスパッチと動的メソッド)

58
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
33