JavaScript をわずかでも速くするために
この記事は公開または最後に更新されてから533日が経過しています。情報が古くなっている可能性があるのでご注意下さい。
JavaScript を、たとえそれがわずかばかりであっても、なんとか速く軽くするためには何をつかったらいいのか?という事と、あらためて向き合ってみたのでその事の備忘録。参考にしているのは、もう、ちょっと古いけれど「 O'REILLY High Performance JavaScript 」の本。
まずは、条件文に関して。
Contents
« if-else or switch »
if-else 文においては、可能性の高いものから条件を並べること。なるべく通過する条件式の数を少なくした方が速くなるというのは、当然の事のように思える。可能性的に均等で条件式の数が多い場合は、工夫して入れ子にし、なるべく通る条件式の数をへらすようにする。
そして、実は if-else よりも switch の方がより高速で、それは条件式が増えるほどに顕著になるということ。これは知らなかった。この本は随分と前に買ったものだけれど、その当時は高速化ということにまで、まだあまり気がまわらなかったのだと思う。
この本自体がもうだいぶ前に出版されたものなので、はたして現在のブラウザにおいても当てはまるものなのか、ちょっと試してみる必要がありそうだ。
比較するのは、if-else 、switch、そして自分としては php 同様にこれがもっとも高速なのだろうと信じ切っていた配列によるルックアップテーブル。配列(オブジェクト)によるルックアップテーブルは、評価する値が条件に完全に含まれている場合とそうでない場合(含まれるかどうかで if 条件文が必要になる)、また値が数値か文字列かでもかなり違いがあるだろうと思われる。if-else、switch においては条件の数を一つへらしてその分を、else、default で補う。これで、if-else、switch においては、評価する値が条件の中に必ずしもあろうとなかろうと関係の無いことになる。
と、いうことで下のコード。
時間を計測する measure 関数は、ネットか本かどこからかいただいてきたものだと思う。忘れてしまった。
基本的には下の感じで、あとは条件を増やしたり、評価値を含まれないようにしたりとか、文字列を数値に変えたりとかで実験した。これで良いと思うのだけれども。swconb というのは、数値で評価する場合に、数値の大小で評価させるもの。これでも違いがあるはず。
見ての通り、それぞれ各100,000回まわして、その結果を10回づつとり平均を出した。
下の表で表題の文字列というのは、評価値が文字列。数字は条件の数。”含”は配列のルックアップテーブルの要素に、評価値を必ず持っているということ。”不”の場合は、評価値が存在しない場合もあり、その条件式が必要になるということ。単位はms 。
測定環境:
Windows10 Core i5-6500 Apache24
FireFox Developer Edition 87.0b5
Google Chrome 88.0.4324.190
array | if | switch | switchb | ||
文字列:3:含 | |||||
FireFox | 3.71 | 2.81 | 2.71 | – | |
Chrome | 4.89 | 3.97 | 3.89 | – | |
文字列:10:含 | |||||
FireFox | 5.18 | 3.61 | 3.40 | – | |
Chrome | 5.78 | 5.05 | 5.23 | – | |
文字列:10:不 | |||||
FireFox | 5.76 | 3.50 | 3.37 | – | |
Chrome | 7.17 | 4.88 | 4.72 | – | |
数値:10:含 | |||||
FireFox | 1.38 | 3.39 | 3.18 | 3.18 | |
Chrome | 2.8 | 4.73 | 4.66 | 2.26 | |
数値:10:不 | |||||
FireFox | 1.99 | 3.19 | 2.95 | 2.9 | |
Chrome | 4.09 | 5.14 | 4.8 | 2.52 |
こうしてみると、条件判断においては FireFox の方が若干ながら高速なよう。評価値が文字列の場合、ルックアップテーブルは連想配列を使うことになり、その場合は if-else、switch 構文よりも遅くなってしまう。が、いざ、評価値に数字を使えるならばただの配列ですみ、この場合は文句なく最速となる。if-else、switch においては、ほんとにわずかながら( 10万回まわしてミリ秒でこれだけしか違わないのだから )switch のほうが速いようだけど、どちらを使っても、という感じがする。特に何もないのであれば、なるべく switch を使うようにして、あとは書きやすさとか読みやすさで選択すればいいのだろうと。
ただ、評価値に数字を使える状況ならば、配列のルックアップテーブルを使うということだと。特に条件の数が多くなればなるほど、その差は大きくなるのだと思われる。
- まとめ:
- 評価値が数値ならば、配列のルックアップテーブルを使う。
- 評価値が文字列ならば、switch を使うようにする。
« ループ は何を使うべきか »
php はシンプルでいいのだけれど、JavaScript の場合は for、foreach、for-of、for-in などなどある。map もあるけれど、ちょっと用途が違うかと思うし、あまり速そうでもないのでこの場合はパス。いろいろあってもその時々のやりたいことで選べばよいのだろうけれど、はたして for だけで全てがまかなえるといえばそうでもありそうだし( for-in は?)。簡単に書けるものが、速さも同等であれば喜ばしいことなんだけれど。
と、いうことで色々並べてやってみようというところ。
これも10万回まわしたのをそれぞれ10回づつ結果をえて、その平均の値。( ms )
FireFox | Chrome | |
obj for | 38.63 | 16.79 |
obj entries | 194.2 | 18.43 |
obj for-of out | 65.16 | 17.17 |
obj for-of | 85.43 | 19.38 |
obj for-in | 39.01 | 1.76 |
obj for-in has | 55.75 | 1.66 |
obj foeach | 72.1 | 19.83 |
ary for | 2.19 | 0.93 |
ary for-of | 32.67 | 1.82 |
ary for-in | 184.9 | 55.75 |
ary foreach | 13.43 | 2.44 |
この結果をみると、条件文の場合とは異なり、ループに関しては圧倒的に Chrome の方が高速らしいということ。特にオブジェクトにおける for-in の速さは圧倒的で使いたくなってしまうほど。hasOwnProperty の条件式をつけてもほとんど変わらないパフォーマンス。そして、「 O'REILLY High Performance JavaScript 」にはこういう注意書きがある。
「 for-in を使って配列の要素を反復処理することは絶対にやめるべきである。 」と。確かに配列においての for-in のパフォーマンスをみると極端に悪くなっている。まぁ、もともと違う用途のためにあるものだし、いらぬものまで入ってきてしまうからなのであろうけれど、オブジェクトに対してであれば、ということなのだろう。この本にも、for-in ループは際立って遅く、その他のループに関してはほぼ同様のパフォーマンスを示すと書いてある。ブラウザの進歩とともに状況は大きく変わってきているようではあるが。なんにしても for-in を使うにしろ、FireFox での hasOwnProperty を使用した時のパフォーマンスの悪さが気になるところではある。
Object.keys + for-of の組み合わせでネットなどでよく紹介されているのは、for の ()の中にObject.keys を入れている書き方。一文ですむのでかっこはいいかもしれない。けれど、これを見てもしや!と思い外に出してみると、案の定、いくらか速くなった。みてくれをとるか実用性をとるかってところ。
連想配列(オブジェクト)でも Object.keys でキーの配列をとってそれをループさせれば、単純な配列のループと大差ないのでは?と思っていたのだけれど、結果は大違い。連想配列にアクセスすること自体が処理を重くさせるのかも。であれば、なるべく連想配列の使用を避けたくなる。それでも連想配列をループさせる必要がある時で、必要なものが値の方だけであるなら、Object.values で値だけの配列をつくってループさせたほうが、速度的には有利なはず。キーが必要なら Object.keys も使って・・・、となるけれど Object.keys の方はキーの並びの順番に関しては保証されないので、数字などキーの値によってはソートされてしまい、元の連想配列との整合性がなくなってしまうから使えない。
まぁ、この結果を見る限り、現状においては、使いなれた for がやはりという感じがするので、連想配列をループさせる場合は、わずかにでも速く!ということなら、ちょっとばかり工夫が必要ということになるか。
あと、ちょ~基本的なことではあるけれど、あれっ!どっちだっけ?となるのが、for-of で得られるのは値の方で、一方の for-in はキー( プロパティ )の方だってこと。
- まとめ:
- ループに限ったことではなく、極力オブジェクト(連想配列)の使用を避ける
- 連想配列をループする必要がある時は、
hasOwnProperty の条件文がいらないことが保証されるときは for-in。でなければ Object.keys を取得して それを for でループさせる。そのループで必要なのが値だけならば、Object.values で値だけの配列を取得してそれを for にてループ。
その後、時が経ち Firefox が v99 となり、hasOwnProperty の条件式をつけても chrome 同等のパフォーマンスを示してくれるようになった。ゆえに、連想配列をループする場合は、迷わず hasOwnProperty の条件式付きでの for-in を使う。おもしろいことに hasOwnProperty の条件式を付けたほうが、Firefox、Chrome 双方ともに速いのである。 - 配列のループは昔ながらの基本、 for の一択
« DOM の更新 »
JavaScript からスタイル設定を変更する場合、それが複数ある場合に、それぞれ一つづつを変更していくのではなく、style.cssText によってまとめて処理をした方が再描画と再配置の回数を減らすことができてパフォーマンス的に有効だとのこと。まぁ、これも感覚的にわかっているようなことではある。
ここで一つ注意しなければならないのは、cssText によってスタイル設定を変更した場合、インラインでスタイル設定してある部分は、上書きされてしまいまっさらになってしまうということ。
インラインでのスタイル設定というと、HTML のタグのなかに直に、style="font-size:1.0em" のようにして書いてあるもののことで、スタイルシートや <head> タグ内に、<style> タグを使って書いてあるものではないということ。よって、スタイルシートや <style> タグで設定してある部分は、cssText によって設定されていないものがなくなってしまったりはしない。はずなんだが・・・。逆に、以前に cssText を使って設定したスタイルは見事に無くなってしまう。
新たに、cssText によって設定する場合、以前に設定した部分を引き続き残しておきたい場合は、+= を使って設定してやれば良い。同じ対象に対して += で何度も設定しても、更新されていくだけで重複してたまってしまうことはない。
一文で設定できるのは、書く方としても助かるのでありがたいのだけれども、今回、ちょっと気になる点もあった。それは Chrome においてのこと。前述したように version は 88.0。アニメーション用に複数の画像を一つにしてbackground として設定し、背景画像の位置を変えることでアニメーションさせていた。アニメーションさせるとともにその要素自体の位置も移動させていたのであるが、これが画像が変わるたびにチラチラと光るようなノイズが出てしまう。FireFox ではそんなことはおこらず、 Chrome であっても cssText を使わず、一つ一つスタイル設定を更新させていけばそんな状況にはならなかった。なにかやり方というか、こつみたいなものがあるのだろうか。
と、いいつつも実はこの件に関しての原因というのは実にシンプルなもので、Chrome の場合、 ディベロッパーツールを表示させていると起こる現象であって、それを閉じていればチラチラノイズが出ることはなかった。まぁ、ディベロッパーツールが相当に負荷が多くなっているということなんだろうけれど。でも、この件において少し気になったこともあるので、今ひとつ、すっきりしない部分ではあるのだけれど・・・。
この件についての続きはこのページにて・・・> 「いまさらの Javascript style.cssText」
« ビット 演算子 »
これまでほとんど接したことのないビット演算。ただ、このビット演算、JavaScript でもその他の数値演算やブール演算子と比べて信じられないほどの高速とのことらしいが。いやはやなんとも・・・。
とにかくわずかでも高速化させたいのであれば・・・、ということで、ならば試してみるしかない。
ビットというと二進法のただ 0 と 1 だけの世界、01010100111 のようなデジタルな世界でなんともわかりづらそうな雰囲気なのではあるけれど、ここでちょっとだけ使うのはもっとわかりやすい単純なものだけ。
ループをさせて、そのなかで交互に処理を変えるなんてのはよくあること。そのとき、2で割った余りの 0 か 1 で条件分岐させるなんてのは常套手段。で、まずはここでビット演算が使えるということ。これも「 O'REILLY High Performance JavaScript 」で教わったことになる。
この場合、必要なのは ビット AND。演算子は &。
これは、対応するビットが両方とも 1 だった場合に 1 を返す。で、2 で割った余りの 0 か 1 かで条件分岐をビット演算ではどう置き換えるのかということを先に下にしめすと・・・、
6行目。実にシンプル。ただ 1 との ビットAND をさせるだけ。
1 は二進法でも 1。で、二進法では偶数の1桁目( 1桁目?でいいのか?なんていうのか?1の位? )は必ず 0。奇数なら必ず 1。これはパソコンの電卓を出してちょっと数字を打ってみればすぐに確認できる。ということで 1 とビットAND をさせれば偶数のときは 0 だし奇数のときは 1 となる、という極めてわかりやすくシンプルなことでそれを利用すべきってことだろうけれど。もともと計算自体が超高速なことだから、これを一つ箇所変えたからと言って体感的にはまったく変わらない。まぁ、すでにあるものをわざわざ変更してなんてものではまったくない。新しく書く場合には、知っていて損はないかもね!という感じのものだと。でも、ビット演算を知っている人には一目瞭然なんだけれど、知らない人にはなんのことかさっぱりだろうからそのあたりも・・・。
これのちょっとした応用としては、たとえば選択子が8個あってそれからランダムに一つ選び出すとか。これも8で割った余りにすれば良いのだけれど、その数が8のような2の累乗であればビット演算が使える。この場合、8より一つ小さいというか8で割った余りの最大値である7でビットANDさせればいい。ある数でビットANDを行うとその数より大きな数字は返ってこない。それが7なら8以上の数は返ってこない。ならばなぜ7なのかというと、それは2の累乗数から1引いた数の2進数をみるとすぐにわかる。1->1、3->0011、7->0111、15->1111、31->00011111、63->00111111、・・・、という具合に 0 と 1 がきれいに並ぶ。ビットAND をさせた場合、こちらが 0 の桁は相手がなんであろうと( って 0 か 1 しかないのだけれど )全く関係なし。こちらで 1 が並んだ桁数だけが有効になるので、こちらが全て 1 ということは、その有効な桁数だけにおいて相手の完全なコピーになるということ。こちらの有効な桁数の中に 0 を含んでいたとすると、相手が 1 でも 0 になってしまうので、結果で得られた数字に偏りが生じてしまう。と、いうことで有効な桁数で全て 1 が並んでいるということが必要になっている。
なんだけれども、実際にベンチで計測してみたところ、普通に割って余りを求めたものとほとんど変わらなかった。というか、余りを求めた方が速いときすらよくあった。ので、現在ではこういうことでビット演算を使う理由もあまりないのかもしれない。ちなみに、php ではどうだろうかと思い、 8 で割った余りで同じく10万回回して試してみたところ、ビット演算のほうが速い結果が出た。もともと超高速なただの計算なので、その差は僅かなものではあるけど。それも続けて計測させると、二度目、三度目の計測ではどんどん差はなくなっていく。まぁ、そんなに感じられるほどの差はないということで、それでもこういう小さなことが積もり積もって影響がでてくるのだろうか。
ということで、この件はこれにておわり。
Post : 2021/03/03 22:42
Comments feed
Trackback URL : https://strix.main.jp/wp-trackback.php?p=164729