immutable.js の Map に対する比較演算子,比較関数の比較と考察

  • 0
    いいね
  • 0
    コメント

    immutable.js の Map に対する比較演算子,比較関数の比較と考察

    はじめに

    この記事は, SLP_KBITその2 Advent Calendar 2016 の 18 日目の記事です.

    サークルのアドベントカレンダーですが,特に気にせず今自分が最も関心の強い分野について,好きに語ります.
    サークル生にとっては,あまりありがたい情報がないかもしれません.

    この記事では,javascriptのライブラリである,facebook/immutable.js で作成した,不変性を持ったオブジェクト(Map) に対しての,
    比較演算子(===) や,比較関数 (immutable.is, Object.is) を用いたときの比較と考察を書き記します.

    記事を読む前に概要を抑えておきたいワードとしては,

    などが挙がります.

    背景

    自分は,React.js を持ちいてフロントエンドを開発するのが好きです.
    Redux を使うときもあれば,flux-utilsのみを用いてシンプルに実装することもあります.
    大学の課題などにも,時々使ったりしています.
    多くのReactを用いたアプリケーションでは,FluxにおけるStoreの情報・状態をComponentで書き換えないために,不変性をもたせることが多いです.そのためのライブラリとして多く使われているのがImmutable.js`です.

    そんなReactですが,アプリケーションが大規模化したときに,ライフサイクルの一つである,
    shouldComponentUpdate の実装が,パフォーマンスの改善のために必要になるケースがあります.
    React@15 からは, react-pure-render-mixin (react-shallow-compare-addons) のような
    実装が初期から入った,PureComponent が提供されたりもしました.(まだ,公式のリファレンスには乗ってない?)

    shouldComponentUpdateの実装で,気をつけなければいけないのが,前回と今回の状態を比較するパフォーマンスです.
    javascriptには,比較演算子が複数種類あるだけでなく,ECMAScript2015から Object.is が入ったりしします.(モダンなブラウザでは,polyfillなしですでに動作しますね)

    今までは,思考停止で Immutable.is という Immutable.js が提供しているAPIを利用して,
    比較を行ってきましたが,Immutable.is の 実装は,hashCode() *1という関数から,
    hash値を算出して,そのhash値が同等かどうかを見ているので比較コストが大きいのではないか?と考えました.

    優位な差が存在するかどうかを,ベンチ取って確かめてみましょう.
    ついでに,Map以外を比較するときに用いる,ライブラリ群も見てみましょう.

    実験

    実験環境

    • Google Chrome : 54.0.2840.98 (64-bit)
    • OS X EL Capitan

      • プロセッサ : 1.6GHz Intel Core i5
      • メモリ : 8 GB 1600MHz DDR3
    • node-modules

      • "benchmark": "^2.1.2"
      • "deep-equal": "^1.0.1"
      • "immutable": "^3.8.1"
      • "lodash": "^4.17.2
      • "browserify": "^13.1.1"

    実験

    シンプルな,Map を比較するとき.(深さ1のオブジェクトをImmutable化して比較)

    const _ = require('lodash');
    const Benchmark = require('benchmark');
    const deepEqual = require('deep-equal');
    const Immutable = require('immutable');
    const suite = new Benchmark.Suite();
    
    const props = {
      a: 1,
      b: "hoge",
    };
    
    const $$props = Immutable.fromJS(props);
    
    const nextProps = {
      a: 2,
      b: "poge",
    };
    
    const $$nextProps = Immutable.fromJS(nextProps);
    
    suite
      .add('===', () => $$props === $$nextProps)
    
      .add('deep-equal', () => deepEqual($$props, $$nextProps))
      .add('Immutable.is', () => Immutable.is($$props, $$nextProps))
      .add('Object.is', () => $$props, $$nextProps)
      .add('_.isEqual', () => _.isEqual($$props, $$nextProps))
      .on('cycle', (event, bench) => console.log(String(event.target)))
      .on('complete', function() {
        console.log('\nFastest is ' + this.filter('fastest').map('name'));
      })
      .run({async: false});
    
    console.log("===          : ", $$props === $$nextProps);
    console.log("deep-equal   : ", deepEqual($$props, $$nextProps));
    console.log("Immutable.is : ", Immutable.is($$props, $$nextProps));
    console.log("Object.is    : ", Object.is($$props, $$nextProps));
    console.log("_.isEqual    : ", _.isEqual($$props === $$nextProps));
    

    結果

    上記スクリプト実行後のコンソールログ
    demo1.png

    Immutableなオブジェクトに対しても,=== できちんと比較できるんですね.

    オペレーション/秒で比較したときの棒グラフ

    スクリーンショット 2016-12-17 22.36.17.png

    グラフを見ると一目瞭然です.仮に,=== を用いて Immutableなオブジェクトの中身が等価かどうかを検証できるのであれば,Immutable.is を使う理由は少なく,
    パフォーマンスの面から考えると,=== のほうがいいのではないのでしょうか
    (再帰的に,values が同じであるかどうかを繰り返し比較している,deepEqualは,結構重いということもあきらかになりました)

    ここで,検討すべきことは,本当に === で すべてのケースの比較を正しく行えるのかどうかです.

    結論から言うと,特定のケースで === ではImmutable.jsで作成した値が同等かどうか比較が不可能です.

    では,=== で Map が同等の値かどうかを検証するにはどうすればよいか.

    値が変わらなければ,propsで渡すMapのインスタンスも変更しなければいいのです.
    つまり,Storeの実装を一工夫するだけで,immutable.is を使わなくても,正確に比較できます.

    具体的には,

    SampleStore.dispachToken = Dispatcher.register((payload) => {
      const action = payload.action;
    
      switch (action) {
        case 'Sample':
          $$state.merge(payload.$$response)
          SampleStore.emitChange();
          break;
      }
    }
    

    こんな感じで,Storeを実装すれば良いです.
    ($$ は Immutable であることを示している.)

    Immutable.js はおそらくそれを前提に作られていて, merge()set() を使って,値を更新するとき,値が前回と同じであれば同じインスタンスが返却されます.

    こうすることによって,比較的低速な Immutable.is を使うことなく, === で Map の比較が可能です.

    まとめ

    • Immutable.is を使った比較は, === より比較的コストがかかっている
    • Immutable な オブジェクトも 工夫すれば === で高速に比較することができる.
    • javascriptにおける比較は結構ややこしい.

    もうちょっと時間あれば,実際にReactのComponentにそれぞれの比較埋め込んでどれくらいレンダリング時のパフォーマンス変わるか見てみたかったですね.

    この投稿は SLP_KBITその2 Advent Calendar 201618日目の記事です。