この記事は、CAM Advent Calendar 15日目の記事です
前回は @matsuhei さんによる、 レガシーコードに対する解析ツール(重複コード編)でした。
概要
以前、Swiftのコードを誰かに見せてたときに以下のように指摘されました。
ここ、
/ 2
になっとるけど* 0.5
のほうが速いで
確かに、基本的には割り算よりも掛け算のほうがパフォーマンスがいいです。直に頭で計算するとき、僕も掛け算のほうが計算しやすいです。計算理由にもよりますが、Swiftもその例から漏れないはずです。
ただ、iOSでは本番用のipaを作成するときにはリリースビルド(本番)が行われます。
最適化の側面から見て、コードが一緒になってないかな?と思ったので、検証してみました。
SIL
注目すべきはSILです。
SILは Swift Intermediate Language
の略で、実行できるバイナリーに落とし込むために必要な中間言語です。
Swift CompilerはLLVM IRに落とし込む前にSILを生成します。
SILは主に2種類、 raw SIL
と canonical SIL
があります。
raw SIL
は、AST(abstract syntax tree)から解析しやすいように変換された直後のものです。
canonical SIL
は、 raw SIL
を最適化された状態の SILです。
検証1
let aaa: Double = 200
let result: Double = aaa * 0.5
let aaa: Double = 200
let result: Double = aaa / 2
実行コード
$ swiftc -emit-sil -O sample1.swift > sample1.sil
$ swiftc -emit-sil -O sample2.swift > sample2.sil
-emit-sil
で canonical SIL
が生成されます。raw SIL
を生成するには、 -emit-silgen
と書けばよいです。
-O
が付いていますが、これは canonical SIL からまた最適化が施された状態のものを生成するために必要なオプションで、ついてない場合は、 -Onone
と同等です。
結果
sil_stage canonical
import Builtin
import Swift
import SwiftShims
@_hasStorage @_hasInitialValue let aaa: Double { get }
@_hasStorage @_hasInitialValue let result: Double { get }
// aaa
sil_global hidden [let] @$s7sample53aaaSdvp : $Double
// result
sil_global hidden [let] @$s7sample56resultSdvp : $Double
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s7sample53aaaSdvp // id: %2
%3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6
%4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
store %5 to %3 : $*Double // id: %6
alloc_global @$s7sample56resultSdvp // id: %7
%8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11
%9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10
%10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11
store %10 to %8 : $*Double // id: %11
%12 = integer_literal $Builtin.Int32, 0 // user: %13
%13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14
return %13 : $Int32 // id: %14
} // end sil function 'main'
sil_stage canonical
import Builtin
import Swift
import SwiftShims
@_hasStorage @_hasInitialValue let aaa: Double { get }
@_hasStorage @_hasInitialValue let result: Double { get }
// aaa
sil_global hidden [let] @$s7sample63aaaSdvp : $Double
// result
sil_global hidden [let] @$s7sample66resultSdvp : $Double
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s7sample63aaaSdvp // id: %2
%3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6
%4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
store %5 to %3 : $*Double // id: %6
alloc_global @$s7sample66resultSdvp // id: %7
%8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11
%9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10
%10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11
store %10 to %8 : $*Double // id: %11
%12 = integer_literal $Builtin.Int32, 0 // user: %13
%13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14
return %13 : $Int32 // id: %14
} // end sil function 'main'
Diff
@@ -9,21 +9,21 @@
@_hasStorage @_hasInitialValue let result: Double { get }
// aaa
-sil_global hidden [let] @$s7sample53aaaSdvp : $Double
+sil_global hidden [let] @$s7sample63aaaSdvp : $Double
// result
-sil_global hidden [let] @$s7sample56resultSdvp : $Double
+sil_global hidden [let] @$s7sample66resultSdvp : $Double
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
- alloc_global @$s7sample53aaaSdvp // id: %2
- %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6
+ alloc_global @$s7sample63aaaSdvp // id: %2
+ %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6
%4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
store %5 to %3 : $*Double // id: %6
- alloc_global @$s7sample56resultSdvp // id: %7
- %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11
+ alloc_global @$s7sample66resultSdvp // id: %7
+ %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11
%9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10
%10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11
store %10 to %8 : $*Double // id: %11
float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100
これ、 200 / 2
した 100
を直接 result に突っ込んでますね……。
最適化された結果、SIL時点で実行時に計算されるのではなく結果のみを返されるようになりました。
リテラル値で計算されるから良いように最適化されたのかなと思ったので、検証コードを変更してみます。
検証2
func test(_ value: Double) -> Double {
return value * 0.5
}
let result: Double = test(200.0)
func test(_ value: Double) -> Double {
return value / 2
}
let result: Double = test(200.0)
結果
sil_stage canonical
import Builtin
import Swift
import SwiftShims
func test(_ value: Double) -> Double
@_hasStorage @_hasInitialValue let result: Double { get }
// result
sil_global hidden [let] @$s7sample36resultSdvp : $Double
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s7sample36resultSdvp // id: %2
%3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6
%4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
store %5 to %3 : $*Double // id: %6
%7 = integer_literal $Builtin.Int32, 0 // user: %8
%8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9
return %8 : $Int32 // id: %9
} // end sil function 'main'
// test(_:)
sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double {
// %0 // users: %3, %1
bb0(%0 : $Double):
debug_value %0 : $Double, let, name "value", argno 1 // id: %1
%2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4
%3 = struct_extract %0 : $Double, #Double._value // user: %4
%4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
return %5 : $Double // id: %6
} // end sil function '$s7sample34testyS2dF'
sil_stage canonical
import Builtin
import Swift
import SwiftShims
func test(_ value: Double) -> Double
@_hasStorage @_hasInitialValue let result: Double { get }
// result
sil_global hidden [let] @$s7sample46resultSdvp : $Double
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s7sample46resultSdvp // id: %2
%3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6
%4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
store %5 to %3 : $*Double // id: %6
%7 = integer_literal $Builtin.Int32, 0 // user: %8
%8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9
return %8 : $Int32 // id: %9
} // end sil function 'main'
// test(_:)
sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double {
// %0 // users: %3, %1
bb0(%0 : $Double):
debug_value %0 : $Double, let, name "value", argno 1 // id: %1
%2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4
%3 = struct_extract %0 : $Double, #Double._value // user: %4
%4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
return %5 : $Double // id: %6
} // end sil function '$s7sample44testyS2dF'
Diff
@@ -9,13 +9,13 @@
@_hasStorage @_hasInitialValue let result: Double { get }
// result
-sil_global hidden [let] @$s7sample36resultSdvp : $Double
+sil_global hidden [let] @$s7sample46resultSdvp : $Double
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
- alloc_global @$s7sample36resultSdvp // id: %2
- %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6
+ alloc_global @$s7sample46resultSdvp // id: %2
+ %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6
%4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
store %5 to %3 : $*Double // id: %6
@@ -25,16 +25,16 @@
} // end sil function 'main'
// test(_:)
-sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double {
+sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double {
// %0 // users: %3, %1
bb0(%0 : $Double):
debug_value %0 : $Double, let, name "value", argno 1 // id: %1
- %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4
+ %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4
%3 = struct_extract %0 : $Double, #Double._value // user: %4
- %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5
+ %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5
%5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6
return %5 : $Double // id: %6
-} // end sil function '$s7sample34testyS2dF'
+} // end sil function '$s7sample44testyS2dF'
%4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5
%4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5
割り算と掛け算がちゃんと別れています。
/ 2
を自動的に * 0.5
に変換される、なんてことはないようです。
ちなみに、test(:)
に @inlinable
を追加したら、検証1と同じ計算した結果の100の値が保持されました。展開されて、最適化が施されたようです。
まとめ
- リテラル値同士の計算は 最適化されて計算した結果のみ保持する
これを色々調べて思ったのは、ただ早いから * 0.5
を選ぶのは軽率かなと思いました。可読性の問題もあったり、上の最適化されて結果同じ場合もあったりするので、そこらへんを正しく見極めて書いていくことが大事だと感じました。
次は16日目、@keitatata による redis レプリケーションとシャーディング です。お楽しみに。
参考資料
以下の資料は今回の記事を書く上で非常に参考になりました。ありがとうございます。
http://llvm.org/devmtg/2015-10/slides/GroffLattner-SILHighLevelIR.pdf
https://blog.waft.me/2018/01/09/swift-sil-1/
https://github.com/apple/swift/blob/master/docs/SIL.rst
https://qiita.com/Kuniwak/items/cbf6b88db249838895b5