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

一昨日、[TweenXCore](http://tweenx.spheresofa.net/core/)というライブラリを公開しました。

ライブラリ自体の解説はリンク先で行っているので、この記事ではTweenXCoreで行った高速化の話を紹介しています。ちょうど、高校数学と`JavaScript`が分かれば理解できそうな内容で面白そうなので取り上げました。

## イージング関数について

イージング関数というのは、モーションに緩急をつけるために使われる関数です。

[Robert Pennerのイージング関数](http://easings.net/ja)というのが有名で、ここで話をする`exponential`(指数)イージングもその一つです。

## expoイージング

[Robert Penner自身の本](http://robertpenner.com/easing/penner_chapter7_tweening.pdf)で紹介されている、``expo``イージングの実装は以下の通りです。

```js
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`の場合で考えてみます。

```js
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*に差し替えるには、以下の公式が使えます。

```math
a^x = e^{x \log_e a}
```

これを先ほどのコードに当てはめると以下の通りです。

```js
Math.easeInExpo = function (t) {
   return Math.exp(Math.log(2) * 10 * (t - 1));
};
```

`Math.log(2)`は定数(`0.6931471805599453...`)なので、さらに以下のように書き換えられます。

```js
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(...)|**75**ms|**114**ms|**588**ms|

3つの環境すべてで効果がありました。

### 検証用のコードと環境

```txt
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](https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/native/java/lang/fdlibm/src/e_exp.c)
[fdlibm/src/e_pow.c](https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/native/java/lang/fdlibm/src/e_pow.c)

明らかに`e_pow.c`の方が複雑な計算をしていて、特殊ケースの場合わけも`e_pow.c`の方が多いということはわかります。

`pow(x, y)`の中身は、

```math
e^{y \log_e x}
```

を計算しているのかと思ってましたが、実際の実装は違うみたいです。(むしろ2の累乗を計算してるっぽい？)

誤差をおさえるためかなと思ったんですが、検索しても資料が出てこなかったので詳しい人がいたら教えて下さい。

## その他の環境

今回試した環境では`Math.exp`を使った方が速かったですが、`Math.exp`を使う必要のない環境もあります。CやRustには2の累乗を計算する``exp2``という関数が用意されているので単にこちらを使えばよいです。

さらに、LLVMには`pow(2, hoge)`というコードを書いたときにコンパイル時に`exp2(hoge)`に変換する機能がふくまれているようです。([参考](http://llvm.org/docs/doxygen/html/SimplifyLibCall))

## 誤差について

`Math.pow(2, 10 * (t - 1))`と`Math.exp(6.931471805599453 * (t - 1))`では、微妙な誤差があります。例えばJavaScriptで`t = 0.2`のときに`3.469446951953614e-18`の誤差がありました。

しかしイージングの用途はアニメーションですから、目に見えない誤差は気にする必要がありません。

それぞれの関数を使ったイージングを作って、曲線を引いて画像を比較しました。

| Math.pow(...) | Math.exp(...) | 差分 |
|:--|:--|:--|
|![pow_expo.png](https://qiita-image-store.s3.amazonaws.com/0/2685/39a0b3ca-d4f0-ad4b-8b2d-340cae23cf05.png)|![exp_expo.png](https://qiita-image-store.s3.amazonaws.com/0/2685/25f8f6f7-95ba-3d82-9a21-640d1118a235.png)|なし（ピクセル単位で一致）|

実用上問題になることは無さそうです。

## 今後について

既存のライブラリを見て回ったんですが以下のすべてで、`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あたりのライブラリ向けにプルリクエストを作ろうかなと思っています。