HTML5 × CSS3 × jQueryを真面目に勉強 – #19 JS の Math 関数を最適化出来ないか検証してみた

2013.10.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回前々回と画像処理について学びました。ピクセル単位での解析をするなど、何かにつけてループ処理を書くことが多くなりがちです。100 回程度のループならまだしも、画像処理となると 1000 回や 2000 回は当たり前、1万回以上ループすることも珍しくありません。そうなるとちょっとした処理の違いが大きなスピードの差を生むことになるわけです。塵も積もれば何とやらです。

で、今回本格的に画像処理をやってみて気づいたのが、Math 関数を結構な頻度で使うんだなぁ、ということでした。Flash(ActionScript) の世界では、ド派手なヴィジュアルでも軽快に動作させるためにループ処理の中では Math 関数の使用を避けるのがセオリーとなっています。では JavaScript の場合はどうなのかいくつかベンチマークをとってみました。

はじめに - Math 関数について

Math を直訳すると数学です。要するに数学的な定数と関数を提供するプロパティとメソッドを持った、JavaScript 組み込みのオブジェクトです。そのためコンストラクタを用いたりメソッドを呼ぶといったことをしなくとも、いきなりアクセスすることが出来ます。

Math オブジェクトには以下の様なプロパティとメソッドが定義されています。

プロパティ

E ネイピア数(オイラー数) - これは自然対数の底として用いられる数学定数で、約 2.718
LN2 2 の自然対数 - 約 0.693
LN10 10 の自然対数 - 約 2.302
LOG2E 2 を底とした E の対数 - 約 1.442
LOG10E 10 を底とした E の対数 - 約 0.434
PI 円周率 - 約 3.14159
SQRT1_2 1/2 の平方根 - つまり、1 割る 2 の平方根。約 0.707
SQRT2 2 の平方根 - 約 1.414

メソッド

abs 引数として与えた数の絶対値を返す
acos 引数として与えた数のアークコサインをラジアン単位で返す
asin 引数として与えた数のアークサインをラジアン単位で返す
atan 引数として与えた数のアークタンジェントをラジアン単位で返す
atan2 引数の比率でのアークタンジェントを返す
ceil 引数として与えた数以上の最小の整数を返す
cos 引数として与えた数のコサインを返す
exp Enumberを返す。ここでの number は引数で、E は自然対数の底である、ネイピア数(オイラー数)となる。
floor 引数として与えた数以下の最大の整数を返す
imul 2 つの引数をとり、C 言語の様な 32 ビット乗算の結果を返す
log 引数として与えた数の自然対数(底は E)を返す
max 引数として与えた複数の数の中で最大の数を返す
min 引数として与えた複数の数の中で最小の数を返す
pow 第一引数の値を第二引数の値で累乗した値を返す
random 0 以上 1 未満の疑似乱数を返す
round 引数として与えた数を四捨五入して、最も近似の整数を返す
sin 引数として与えた数のサインを返す
sqrt 引数として与えた数の平方根を返す
tan 引数として与えた数のタンジェントを返す

Math 関数と最適化したコードのベンチマークをとってみる

いくつかの Math 関数を最適化したコードで書き直すとどれくらい速度に差が生まれるのか、実際にベンチマークをとって検証してみたいと思います。この辺りのノウハウは、JavaScript 関連よりも Flash 関連で探したほうが有益な情報が多く見つかったりするので、それらを参考にしています。言語は違いますがどちらも ECMAScript 準拠なので、大概はそのままコピペで流用することが出来るんですよね。

レガシーなブラウザによっては予期せぬ動作をする可能性があるので、その辺りのチェックは忘れずに。

Math.floor();

小数点以下の数値を切り捨てる時は Math.floor(); を使いますが、以下のような論理和を用いたコードで同様の処理が実現できます。

数値 | 0;

他にもビット演算を使うことで実現することも出来ます。

数値 >> 0;

数値を 0 ビット右にシフトするというものです。理論上はビット演算の方が速いということになります。

ベンチマーク

即席ですが、ベンチマーク測定用のコードを組みました。

測定内容
  • Math.floor();とそれに代わる処理をそれぞれ1000万回呼びだし、所要時間を測定

こちらより実際に測定することが出来ます。

いかがでしたでしょうか。ちなみに僕の環境で測定した結果は以下の通り。

PC 環境

OS
OS X 10.9 Mavericks
CPU
2.4 GHz Intel Core i7
RAM
8 GB 1600 MHz DDR3
Browser
Chrome 30.0
Firefox 24
Safari 7.0
Chrome Canary 32
WebKit Nightly Builds
InternetExplorer 10 *1

iPhone 環境

機種
iPhone 5
Browser
Safari
Chrome

PC 環境

img-math_floor

iPhone 環境

img-math_floor_ios

PC 版 Chrome のみ Math 関数の方が速いという結果になりました。正しく言うならば、Chrome に搭載されている V8 JavaScript Engine の測定結果ということになります。この現行版 Chromeよりも 2 バージョン先の ChromeCanary では他のブラウザ同様、ビット演算と論理和のほうが速いというのもなかなか興味深いです。

Math.round();

整数の四捨五入をするための関数です。上記の小数点以下切り捨てのテクニックを応用することで、こちらも Math 関数を使わずに実現することができます。

(数値 + 0.5) | 0;

同様の方法でビット演算でも実現できます。

(数値 + 0.5) >> 0;

こちらより実際に測定することが出来ます。

僕の環境で測定した結果は以下の通り。

PC 環境

img-math_round

iPhone 環境

img-math_round_ios

Math.floor(); と似たような結果ですが、Math.floor();と違ってChromeの結果が逆転しています。何度も測定してみましたが、ビット演算と論理和のほうが速いという結果は変わりませんでした。

Math.max();, Math.min();

2つの数値から最大値もしくは最小値を求める関数として Math.max(); と Math.min(); があります。やや冗長な書き方になりますが、条件演算子の : ? を利用することで同様のことが実現できます。

// MAth.max(a, b); (a > b) ? a : b;

// Math.min(a, b); (a < b) ? a : b; [/javascript]

こちらより実際に測定することが出来ます。

僕の環境で測定した結果は以下の通り。

PC 環境

img-math_max

iPhone 環境

img-math_max_ios

今度は Firefox において Math 関数と条件演算子の結果がほかブラウザと逆になっています。

Math.abs()

数値の絶対値を求めるための関数です。Math.max()、Math.min() 同様、やや冗長なので場合によっては可読性が大きく損なわれることになりますが、結構なパフォーマンス改善が期待出来ます。

(数値 < 0) ? -数値 : 数値; [/javascript]

こちらより実際に測定することが出来ます。

僕の環境で測定した結果は以下の通り。

PC 環境

img-math_abs

iPhone 環境

img-math_abs_ios

おわりに

こうしていざ実際に試してみると、Flash と違ってブラウザや Math 関数によって結果が異なるというのが意外でした。今回試した方法は関数の呼び出しをせずに式をステートメントとして直接書くことで高速化を実現するというものでした。一度きりの呼び出し箇所ならば大したチューニングにはならないので、可読性を優先させてMath関数を呼び出してしまったほうがメリットが大きいですが、塵も積もれば山となるというやつで、回数が多く呼ばれるようなループ処理の中といった際には、こういったチューニングを実践してみるのが良いかと思います。もちろん動作保証するブラウザを勘定にいれた上で実装するかどうかを判断するべきですが。

参考サイト

高速化

脚注

  1. VMWare Fusion で仮想マシンを立ち上げ、そこでWindows環境を動かしています。