変数の存在を確認する方法と速度比較(その2)

2007.12.11 / javascript

前回に「関数の引数を存在確認するための高速化Tips」と題したエントリーを書きましたが、その後にgakkieからいろいろ意見をいただきました。

@ryo_katsuma 露骨ってほどの差には見えないがw 条件文がtrueでも同じ結果になる?<三項演算子

Twitter / gakkie

@ryo_katsuma ついでに、10万回で50msの違いっていうのはJSにおいて重要なのかがちょっと気になったお。

Twitter / gakkie

あと、その後にチャットで話した結果、自分自身で納得したのですが、前回のエントリーで言いたかったことは「関数の引数うんぬんとかはどうでもよくて、"変数が存在するかどうか" を考えたとき、その確認方法によっては速度が変わることがある」と、いうことが一番言いたかったんだな、、ということに自己完結。また、確認方法についても「関数呼び出しを行う時間を含める/含めない、によっても純粋な判定時間とはずれる?」と、いう話にもなったり。

と、いうことで「関数呼び出しを含まない」「真偽の2パターン」を考慮してもう一度測定し直してみました。検証コードは次のとおり。

  const LOOP = 10000000;
  var start, end;
  
  // logging
  function log(){
      //return console.log.apply(this, arguments);
      return print.apply(this, arguments);
  }
  
  
  
  function checkStrTernary(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret = (arg=='foo')? arg : 'bar';
      }
      end = new Date();
      log('[checkStrTernary]\t' + (end-start));
  }
  
  
  function checkStrIf(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret;
          if(arg=='foo'){
              ret = arg;
          } else {
              ret =  'bar';
          }
      }
      end = new Date();
      log('[checkStrIf]\t'+ (end-start));
  }
  
  
  function checkArgsLogicalAdd(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret =  arg || 'bar';
      }
      end = new Date();
      log('[checkArgsLogicalAdd]\t ' + (end-start));
  
  }
  function checkArgsTernary(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret =  arg ? arg : 'bar';
      }
      end = new Date();
      log('[checkArgsTernary]\t ' + (end-start));
  
  }
  function checkArgsIf(arg){
      start - new Date();
      for(var i=0; i<LOOP; i++){
          var ret;
          if(arg) {
              ret = arg;
          } else {
              ret = 'bar';
          }
      }
      end = new Date();
      log('[checkArgsIf] \t' + (end-start));
  }
  
  
  function checkNullTernary(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret = (arg!=null)? arg : 'bar';
      }
      end = new Date();
      log('[checkNullTernary] \t' + (end-start));
  }
  
  function checkNullIf(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret;
          if(arg != null){
              ret = arg;
          } else {
              ret = 'bar';
          }
      }
      end = new Date();
      log('[checkNullIf] \t' + (end-start));
  }
  
  
  function checkTypeTernary(arg){
      start = new Date();
      for(var i=0; i<LOOP; i++){
          var ret = (typeof(arg) != 'undefined')? arg : 'bar';
      }
      end = new Date();
      log('[checkTypeTernary] \t' + (end-start));
  }
  
  function checkTypeIf(arg){
      var ret;
      start = new Date();
      for(var i=0; i<LOOP; i++){
          if(typeof arg != 'undefined'){
              ret = arg;
          } else {
              ret = 'bar';
          }
      }
      end = new Date();
      log('[checkTypeIf] \t' + (end-start));
  }
  
  /*
   * Testing
   */
  (function(arg){
      checkStrTernary(arg);
      checkStrIf(arg);
      checkArgsLogicalAdd(arg)
      checkArgsTernary(arg);
      checkArgsIf(arg);
      checkNullTernary(arg);
      checkNullIf(arg);
      checkTypeTernary(arg);
      checkTypeIf(arg);
  })(/*'foo'*/);
  

存在確認の中では関数呼び出しは行わないように。あと、コード簡単だから解説なんて必要ないと思いますけど、上から順番に

  • 文字列比較(三項演算子)
  • 文字列比較(if文)
  • 変数比較(論理和)
  • 変数比較(三項演算子)
  • 変数比較(if文)
  • null比較(三項演算子)
  • null比較(if文)
  • 型比較(三項演算子)
  • 型数比較(if文)

それぞれ変数に何か値(か、fooの文字列の場合も含)があればtrue, 無かったらfalseの場合で比較。結果は次の通り。単位は1000万回実行したときの秒数です。実行環境はFedora7上のSpiderMonkeyです。

TRUEFALSE
checkStrTernary46874693
checkStrIf48434935
checkArgsLogicalAdd37953791
checkArgsTernary39853993
checkArgsIf82758400
checkNullTernary47264452
checkNullIf45434648
checkTypeTernary51175684
checkTypeIf54225469

考察

前回と結局のところ内容はほぼ変わりませんが、面白い点もいくつかあります。

  • 同じ比較方法の場合、if文よりも三項演算子の方がやや速い
  • もっとも高速な方法は論理和を使う方法
  • もっとも時間がかかる方法は型比較をif文で行う方法
  • null比較、型比較で三項演算子を利用すると真偽によって速度が(割と)変わる

三項演算子を使うと速さが変わるのは前回のエントリでHaraさんがTBくれて言及していただけました。

if文は「文」なのに対し、三項演算子は「式」なので、解釈系(インタプリタ)や翻訳系(コンパイラ)が分岐除去の最適化をしやすいんじゃないかと思います。

if文と三項演算子 - Pa works

文よりも単純な構成である式の方が分岐除去の最適化が効いてくるんじゃないか?な推測。うーん、コンパイラな下地がある方のお言葉はありがたい。。。あと、型比較やnull比較において真偽で時間に差がついてきちゃうのも、分岐除去にクセがあるのかもしれないですね。このあたりの話はSpiderMonkeyのソース読めば追いかけられるのかなぁ。今後ちゃんと読んでみよう。

# そういえば、某大手Webサービスのエンジニアの中には、三項演算子マニア?がいるそうで、ほぼ全ての分岐は三項演算子で書くとか(!)

ざっくりまとめ

JavaScriptでは簡単な分岐なら三項演算子を積極的に使っても損は無いと思います。また変数の初期化なんかは論理和演算子での初期化も良さそう。

他の言語

解釈系、翻訳系で違いを見てみたいなぁ。翻訳系の方が直感的に考えても分岐除去の最適化を頑張ってくれてるはずなので、差が無いと思うんだけども。これも今度ベンチとってみよう。