expoイージングの高速化の話


一昨日、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が移動時間です。

このうち、bcdはイージング曲線の形状とは関係ない平行移動や拡大縮小を行うための値なので、話を分かりやすくするため、b = 0c = 1d = 1の場合で考えてみます。

Math.easeInExpo = function (t) {
   return Math.pow(2, 10 * (t - 1));
};

この関数でボトルネックになるのはMath.powですから、今回の話もそこが焦点になります。

Math.powMath.exp

JavaScriptC#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の実装

powexpの実装について、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あたりのライブラリ向けにプルリクエストを作ろうかなと思っています。