次回のオンライン・オフィスアワーは 7/16(木)14:00〜です。 何でもお気軽にご相談ください!
Realmは、SQLiteやCoreDataから置き換わるモバイルデータベースです。

Swift はどれくらい Swift(速い) ですか?

a video from the Swift Summit Conference

どれくらい Swift は Swift(速い) ですか? Swift はスピードのことを考えデザインされた言語です。しかし、明らかにまだ最適化の余地がある部分がたくさん残っているように思えます。この発表では、Joseph Lord が Swift のパフォーマンスで気を付けるところや、どのようにすると Swift がより速くなるのかについて自身の経験から得た知見を共有しています。C と C++ との比較などを含む、ご自身で行った最適化の実験を用い、ここ最近の Swift のパフォーマンスの改善について学び、あなたの Swift がよりパフォーマンスが良い Swift になることかと思います。

この発表に関する解説記事も Joseph のブログにあります。


スピードのための設計 (0:13)

Swift は速く動作するように考え、デザインされた言語です。値型や静的型付けはスピードを改善する点で非常に良い助けとなっています。今までとは違い、必要に迫られて参照型を使わない限り、プログラム中から参照型を無くすことができるようになります。また、不変性や Copy-on-Write なども非常にパフォーマンスを向上させる上で重要なものです。

私は去年の8月頃から Swift のパフォーマンスについて見てきました。そして、スピードの観点からまだまだ改善の余地があると感じました。たくさんの人が実験を行いましたが、コンパイルされたコードは期待しているほど他の言語に比べ、速くは動作しませんでした。そこで、自分で何かパフォーマンスの改善ができないか Swift を調べることにしました。そして、たくさん改善できる部分を見つけました。他の人のコードを見比べていくことで、どこが最適化できるのか理解していきました。開発者が問題を抱えたり、不満を言っている部分を探し、何がパフォーマンスを悪くしているのか調べるようにしていました。

この発表で使うものは最新版である Xcode 6.3 beta 2 に入っている Swift だとお考えください。パフォーマンス改善の歴史を見るために、それより以前の Swift と比較して説明しているところもあります。Swift の驚くべき開発ペースで、Swift 1.1 でパフォーマンス的に少し手を加えないといけなかったところも Swift 1.2 ではすでに解消されていたりしました。Swift 1.2 beta1 のデバッグビルドはかなり速くなり、beta 2 では以前にやらなければいけなかったことがさらに省けるようになりました。非常に良い発展だと思います!

最適化とは (2:32)

言語に静的なディスパッチを取り入れることで動的な部分が減らせ、パフォーマンスの改善に非常に重要なインライン展開が行えるようになります。

そのため Objective-C ではできなかったたくさんの最適化が可能になりました。しかし、ほとんどのアプリでそこまで深い部分での最適化は必要ないと思っています。なぜならアプリの大半の処理は API コールやネットワーク処理、ブロッキングI/O、ライブラリの処理です。アプリの最適化を考えた時、まずそのような部分から最適化をし始めるべきです。しかし、今回はその辺りのことについては触れないことにします。そして、次に考えることは Swift のビルド設定の見直しです。デバッグビルドではリリースビルドに比べるとデバッグに必要な余計なコードが含まれるためかなり遅くなってしまいます。デバッグで実行時のデータを参照する必要があるため、パフォーマンスにかなり影響が出てくるのです。

また次に考えることとして、どの部分が遅いのか原因を調べることです。たとえ100倍スピードが上がったとしても、それがごく稀にしか起こらなかったら、正しく最適化を行えたとは言えません。プロファイラを取るツールで Xcode の Instruments をご存知かと思います。実際のところ何でもかんでも読めるというわけではありませんが、パフォーマンス関連のことを行うのであれば、知っておくことは価値のあることだと思います。プログラムのどこに問題があるのか知ることはかなり有益なことです。たとえばインライン化したいと思っていた関数呼び出しを調べたりすることができます。

それに加えて、他にもいくつか Swift コードを速くする方法はあります。また、別に深刻なパフォーマンスの問題に直面したとき、MetalAccelerate や並列処理を導入するという選択肢もあります。

ビルド設定 (4:30)

デバッグビルドのデフォルトは -Onone です。これは Swift 1.2 でさえパフォーマンスにクリティカルなコードではかなり遅くなります。リリースビルドのデフォルト設定は -O で、これはかなり速いです。-Ounchecked にすると、前提条件(precondition)のチェックが無くなります。今のところそれほど大きな違いはありませんが、チェックを省くと 10% ほど速くなります。

インクリメンタルビルドがオフになってしまいますが、Swift 1.2 では whole module optimization が指定できるようになりました。これを有効にすると一回のコンパイルですべてのモジュールまたはアプリのコードがビルドされるようになるため、ビルド時間が遅くなります。ビルド時間は遅くなりますが、すべてのファイル間でインライン化や最適化ができるようになります。有効にすることでコードを移動させる必要がなく、良いパフォーマンスが得られます。これはかなり大きな影響があります。また、ジェネリックスを使った関数定義などでも役に立つと思います。

主要なものにクラスを使ったときと構造体を使ったときを比較すると、クラスを使ったプログラムはかなり遅くなります。また、CPU を集中的に使うような箇所では、最適化は非常に重要なことです。私が行ったテストでは、最適化を行っていないコードでは30分ほどかかったのに対し、最適化を行うとなんと40秒ほどで終わりました。このことは、デバッグビルドがかなり違うことを普段、裏で行われていることを示してと思います。ここで一つ教訓として、プロジェクトがある程度大きいとプログラム全体ではデバッグを行わずに、ある部分でのみデバッグビルドにしたいと思うときがあります。

2つのターゲットを作成 (7:30)

特定の部分にのみデバッグビルドを適用するためには、アプリケーションのターゲットを2つに分けることで実現できます。フレームワークのターゲットを別に作り、メインのアプリでそれを使います。特にパフォーマンスのクリティカルなコードはそこに含み、そのフレームワークの中に入れてしまいます。このようにして whole module optimization を有効にすることでフレームワークのコードだけに対し最適化が行え、他のコードはデバッグビルドのままに保てます。そして、リリースビルドを行うときは、すべてのコードに対し最適化を行います。これは、デバッグビルドと最適化するビルドをプロジェクト内で混在させる方法です。少し面倒に感じるかもしれませんが、ビルドが遅いことを解消したいが、デバッグビルドを部分的に持っていたいという場合には、役に立つ方法です。

Swift は Objective-C より速い? (8:46)

大きな疑問として、Swift の C は Objective-C よりも本当に速いかというものがあります。質問はシンプルですが、答えはそうではありません。C は Objective-C のサブセットなので Objective-C と言えますし速いです。しかし NSObject ベースの NSArray などは ARC によって retain され release されるので、コードのスピードは非常に遅くなります。あまり役に立たない答えかもしれませんが、結局あなたが書く Objective-C に依存するということになります。構造体を使った Swift の場合は、C に近いスピードになり、Objective-C よりもかなり速くなるでしょう。

ネット上にある他人のプロジェクトの Swift と C と C++ を比較したとき、いくつか例外はありましたが大体の場合、20%以下ぐらいの違いがありました。Geekbench の開発元である Primate Labs は C++ のベンチマークと比較した結果について書かれた記事とともに Swift のパフォーマンステストを行うソースコードを公開しています。最新のベータで3つの実験を行っています。記事にある FFT と Mandelbrot でのテストは Swift と C++ で似たような結果になっています。しかし、GEMM のテストでは C++ のほうが4倍ほど速くなっています。一つの理由として “fast math” を使ってテストをしているところです。これは正確性が失われる代わりに一層速い最適化が行えます。こういったことは Swift では行えません。

その他に David Owens が行った C の方が7倍速くなる実験結果があります。彼の4つのブログ記事(Swift Resistance, Swift Resistance Explored, The Optimization Game, Swift v1.2 Performance)で C の方がかなり速くなるケースについて説明されています。これらのテストでは C で -O の最適化が使われていますが、もっとよく使われるであろう -Os を使ってビルドしたとき、C は2倍ほどしか速くなりませんでした。これでも十分速いと言えるかもしれませんが、Swift にはまだまだ改善できる余地があります。様々な点から見て Swift には、まだまだ C ほどのパフォーマンスが十分に期待できる可能性があると思います。

シンプルな最適化 (11:35)

さて、他にもどのようにすれば Swift がより Swift(速く)になるでしょうか? いくつかの点については既にお伝えしました。簡単なことから始めると、使えるところでは構造体を使うことです。これでオブジェクトを使うときよりも速くなるでしょう。また、オブジェクトを使うわなければいけないところでは、できるだけ final を指定するようにしてください。そうすることで、コードを直接呼ぶようになり動的な部分が減らせます。次に let を使うようにします。これは最適化という点だけで良いというわけではありません。私の考えでは、コード全体のデザインも改善されます。クラスが増えていくことに連れ、設計に余程気を遣わない限り、メソッドが他の開発者によって予想もしないオーバライドが行われるリスクが生じてきます。特にオブジェクトに変化を加えるようなものなどです。final は正確さを維持するのにとても良いです。構造体にも同じことが言えます。

いくつか危険な最適化もできます。&+, &-, &* オペレーターはオーバーフローチェックを行いません。これを使うときは自分でオーバーフローのリスクを管理しなければいけません。また、Unmanaged<T> を使うことで ARC に管理されないオブジェクトが作れ、スピードを上げることができます。しかし、オブジェクトがリリースされないことに注意する必要があります。

避けるべきこと (13:39)

以上のようなことは高いパフォーマンスを出すためには重要な事です。いつでも書いたコードにとって適切なことを行うようにしてください。最適化について考えすぎてしまうこともあまり良くないことです。さらにもう少しクリティカルな部分について考えた時、良くないことがいくつかあります。グローバルクラスと static 変数 へのアクセスはスピードが遅くなる恐れがあります。これらは他の場所で変更が加わる可能性があるので、コンパイラは最適化を行うことができません。他にもパフォーマンスにクリティカルなコードでは、インライン化することができない関数呼び出しをできるだけ避けるようにしてください。理想的には、関数呼び出しはコストがかかるため全体的に行いたくないです。また、できるのであれば、プロトコルや final でないクラスメソッドやプロパティや Objective-C と互換性のあるものを通してのアクセスを避けるようにしてください。

チェックリスト (14:49)

ビルド設定がどうなっているのかチェックする。それとプロファイルを取ってみてください! クリティカルな部分を計測しておくことで、加えた変更が意味があったのか判断することができます。

以上です、ありがとうございました!


この発表の解説として、Joseph のブログ記事をご参照ください。この最適化テストの手本とした Cellular Automata のコードの作者の Simon Gladman にもお礼を言っておきましょう!

Q&A (15:15)

Q: どのようにすれば関数がインライン化されていることを確認できますか?

Joseph: コンパイラがどこで関数が呼ばれているのか正しく分かっているのか考える必要があります。whole module optimization を有効にすることで、同じファイルかモジュールにでき、まずそれを行う必要があります。いくつか例外はあるみたいですが、それについて私はよく知りません。クラスには final が付けられている必要があり、そうでないと継承される可能性について考えなければいけないからです。最新のベータでは、同じモジュールで internal であるもしくは、同じクラスで private である場合は、自動的に final としてマークが付けられます。それらが継承されないことが保証された時にコンパイラは正しく働きます。このようにコンパイラが関数がどこで呼び出されるのか完全に分かっているのか調べる必要があります。そうすればプロファイラを使うか、SIL コードを見るか、アセンブラを見ていくのか判断できると思います。

Q: Swift の最適化について考えた時に、今の段階で注意すべき部分などはありますか? また、どのようにしてその部分はテストできますか?
Joseph: 前提条件のチェックを外す場合のみ振る舞いが変わる可能性があると思います。しかし、コンパイラはそれが正しいと思い込んでいます。他の結果が特に振る舞いに影響があるとは思いません。チェックをしなくなると別のものになってしまい、その時、どのチェックをスキップするかなどの正確なコントロールは行えません。

Q: 去年、float が int に置き換えられるという大きな問題がありました。Apple は大きな改善を加えたなどで、これについて今でも何か関係があることはありますか??
Joseph: はい、確かにキャストしている箇所で問題を見つけました。Foundation クラスにブリッジしているところでタイポしているのが原因でした。一度 NSNumber に変換し、元に戻すやり方で問題なく動いたと思います。いくつかのケースでまだうまく行かなかったと思いますが、新しいベータでは改善されているはずです。Foundation の型から Swift の型に自動的にはキャストされないのが原因でした。特に Swift 1.1 で Foundation フレームワークを読み込むときは注意してください! 異なる型で NSNumber を通してキャストしているため型セーフでなくなっています。また、オブジェクトを使っているため遅くなります。

Q: Swift 1.0, 1.1, 1.2 で同じテストのコードを実行しましたか? もし行ったのであれば何か違いはありましたか?
Joseph: 直接的な比較を行ったことはありません。Geekbench のテストコードで、いくつかの結果がありました。覚えている限りでは、Swift 1.1 と 1.2 のベータではかなり速くなっていたと思います。Swift 1.2 の beta 1 と beta 2 で以前にひどかった部分が改善されたからだったと思います。final が自動で付くようになりましたし、以前なら Array の代わりに UnsafeMutableBufferPointer を使っていました。Swift 1.1 ではかなりパフォーマンスに影響があったからです。Swift 1.2 ではそのような違いはなくなり、Array が速くなりました。以前は、チェックしすぎな部分がありました。しかし今では、十分なチェックがあり、その上スピードが改善されています。どれくらいの改善が見れるかはどの時点から Swift の開発を始めたのかによります。beta2 と Swift 1.1 とを比べたとき、10倍以上の最適化が見込めますが、提案したような最適化を既にしているのであれば、それほどの改善は期待できないかもしれません。