一昨日、TweenXCoreというライブラリを公開しました。
ライブラリ自体の解説はリンク先で行っているので、この記事ではTweenXCoreで行った高速化の話を紹介しています。ちょうど、高校数学とJavaScript
が分かれば理解できそうな内容で面白そうなので取り上げました。
イージング関数について
イージング関数というのは、モーションに緩急をつけるために使われる関数です。
Robert Pennerのイージング関数というのが有名で、ここで話をするexponential
(指数)イージングもその一つです。
expoイージング
Robert Penner自身の本で紹介されている、expo
イージングの実装は以下の通りです。
Math.easeInExpo = function (t, b, c, d) {
return c * Math.pow(2, 10 * (t/d - 1)) + b;
};
t
が現在時間、b
が開始位置、c
が移動量、d
が移動時間です。
このうち、b
、c
、d
はイージング曲線の形状とは関係ない平行移動や拡大縮小を行うための値なので、話を分かりやすくするため、b = 0
、c = 1
、d = 1
の場合で考えてみます。
Math.easeInExpo = function (t) {
return Math.pow(2, 10 * (t - 1));
};
この関数でボトルネックになるのはMath.pow
ですから、今回の話もそこが焦点になります。
Math.pow
と Math.exp
JavaScript
、C#
、Java
などの言語には指数をあつかう関数がMath.pow(底, 指数)
の他にもう一つあります。それがMath.exp(指数)
です。これはe(ネイピア数)を底とする指数です。
指数の底をa
からeに差し替えるには、以下の公式が使えます。
a^x = e^{x \log_e a}
これを先ほどのコードに当てはめると以下の通りです。
Math.easeInExpo = function (t) {
return Math.exp(Math.log(2) * 10 * (t - 1));
};
Math.log(2)
は定数(0.6931471805599453...
)なので、さらに以下のように書き換えられます。
Math.easeInExpo = function (t) {
return Math.exp(6.931471805599453 * (t - 1));
};
これが、今回行った高速化です。ちなみにElastic
(ばね)イージングについてもMath.pow(2, ...)
が出てくるので、同様の高速化が可能です。
実際にベンチマークを取る
実際に各プラットフォームでの実行速度を測ってみます。
Math.pow(2, 10 * (t - 1))
とMath.exp(6.931471805599453 * (t - 1))
を、それぞれ10000000
回計算してその結果を以下に表にまとめました。
結果
JavaScript(Node) | C# | Java | |
---|---|---|---|
Math.pow(...) | 789ms | 405ms | 741ms |
Math.exp(...) | 75ms | 114ms | 588ms |
3つの環境すべてで効果がありました。
検証用のコードと環境
OS : Windows 10 Home (バージョン1607)
プロセッサ : Intel(R) Core(TM) i7-6700HQ
Haxe : 3.3.0-RC.1 (hxcs:3.2.0, hxjava:3.2.0)
node : v4.6.1
java : 1.8.0_111
.NET Framework : 4.0.30319.42000
Math.pow
の実装
pow
とexp
の実装について、OpenJDKの実装が以下で見れました。
fdlibm/src/e_exp.c
fdlibm/src/e_pow.c
明らかにe_pow.c
の方が複雑な計算をしていて、特殊ケースの場合わけもe_pow.c
の方が多いということはわかります。
pow(x, y)
の中身は、
e^{y \log_e x}
を計算しているのかと思ってましたが、実際の実装は違うみたいです。(むしろ2の累乗を計算してるっぽい?)
誤差をおさえるためかなと思ったんですが、検索しても資料が出てこなかったので詳しい人がいたら教えて下さい。
その他の環境
今回試した環境ではMath.exp
を使った方が速かったですが、Math.exp
を使う必要のない環境もあります。CやRustには2の累乗を計算するexp2
という関数が用意されているので単にこちらを使えばよいです。
さらに、LLVMにはpow(2, hoge)
というコードを書いたときにコンパイル時にexp2(hoge)
に変換する機能がふくまれているようです。(参考)
誤差について
Math.pow(2, 10 * (t - 1))
とMath.exp(6.931471805599453 * (t - 1))
では、微妙な誤差があります。例えばJavaScriptでt = 0.2
のときに3.469446951953614e-18
の誤差がありました。
しかしイージングの用途はアニメーションですから、目に見えない誤差は気にする必要がありません。
それぞれの関数を使ったイージングを作って、曲線を引いて画像を比較しました。
Math.pow(...) | Math.exp(...) | 差分 |
---|---|---|
なし(ピクセル単位で一致) |
実用上問題になることは無さそうです。
今後について
既存のライブラリを見て回ったんですが以下のすべてで、Math.pow(2, ...)
の計算が使われていました。
プラットフォーム | ライブラリ |
---|---|
Flash | Tweener, Tweensy, Tween24, BetweenAS3 |
Unity | iTween, Uween, GoKit |
JavaScript | tween.js, jQuery Easing |
Haxe | Actuate, Delta |
深い理由があるわけではなく、Math.exp
で書き換え可能なのとMath.exp
の方が速いのがあまり知られていないだけじゃないかと思うので、Actuate、tween.jsあたりのライブラリ向けにプルリクエストを作ろうかなと思っています。