最適化から見る、Swiftの / 2 と * 0.5

この記事は、CAM Advent Calendar 15日目の記事です:golf:
前回は @matsuhei さんによる、 レガシーコードに対する解析ツール(重複コード編)でした。

概要

以前、Swiftのコードを誰かに見せてたときに以下のように指摘されました。

ここ、 / 2 になっとるけど * 0.5 のほうが速いで

確かに、基本的には割り算よりも掛け算のほうがパフォーマンスがいいです。直に頭で計算するとき、僕も掛け算のほうが計算しやすいです。計算理由にもよりますが、Swiftもその例から漏れないはずです。

ただ、iOSでは本番用のipaを作成するときにはリリースビルド(本番)が行われます。
最適化の側面から見て、コードが一緒になってないかな?と思ったので、検証してみました。

SIL

Swift Compiler は以下のようになっています。
SS 2019-12-14 14.05.03.png

注目すべきはSILです。
SILは Swift Intermediate Language の略で、実行できるバイナリーに落とし込むために必要な中間言語です。
Swift CompilerはLLVM IRに落とし込む前にSILを生成します。
SILは主に2種類、 raw SILcanonical SIL があります。
raw SIL は、AST(abstract syntax tree)から解析しやすいように変換された直後のものです。
canonical SIL は、 raw SIL を最適化された状態の SILです。

検証1

sample1.swift
let aaa: Double = 200
let result: Double = aaa * 0.5
sample2.swift
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-silcanonical SIL が生成されます。raw SIL を生成するには、 -emit-silgen と書けばよいです。
-O が付いていますが、これは canonical SIL からまた最適化が施された状態のものを生成するために必要なオプションで、ついてない場合は、 -Onone と同等です。

結果

sample1.sil
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'
sample2.sil
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

sample3.swift
func test(_ value: Double) -> Double {
  return value * 0.5
}
let result: Double = test(200.0)
sample4.swift
func test(_ value: Double) -> Double {
  return value / 2
}
let result: Double = test(200.0)

結果

sample3.sil
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'
sample4.sil
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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account