読者です 読者をやめる 読者になる 読者になる

クロージャとbindの引数束縛パフォーマンス

JavaScript node.js

JavaScriptで引数を束縛したい場合、クロージャかbindを使うのだが…

function plus(x, y) {
    return x + y;
}

function multiply(x, y) {
    return x * y;
}

// Function.prototype.bindで束縛
const plus1 = plus.bind(null, 1);

plus1(5);    // => 6


// closureで束縛
function closure(func, param) {
    return function(index) {
        return func(param, index);
    };
}

const twice = closure(multiply, 2);

twice(5);    // => 10

なんとなく、どちらが速いのか気になったので比べてみた。

console.time('bind');
for (let i = 0; i < 100000; i++) {
    const plusX = plus.bind(null, i);
    const multiplyX = multiply.bind(null, i);

    for (let j = 0; j < 10; j++) {
        plusX(j) + multiplyX(j);
    }
}
console.timeEnd('bind');


console.time('closure');
for (let i = 0; i < 100000; i++) {
    const plusX = closure(plus, i);
    const multiplyX = closure(multiply, i);

    for (let j = 0; j < 10; j++) {
        plusX(j) + multiplyX(j);
    }
}
console.timeEnd('closure');

結果

$ nvm use v6
Now using node versions/node/v6.2.1

$ node speed.js
bind: 106.394ms
closure: 33.421ms

node v6.2.1ではクロージャを使うほうが3倍ほど高速らしい。

node ver.4でも測定

$ nvm use v4
Now using node versions/node/v4.4.5

$ node speed.js
bind: 949ms
closure: 33ms

node ver.4では30倍程度の差がついた。というよりも、ver.4とver.6でFunction.prototype.bindが10倍も高速になっている。

node v4.4.5にはV8 v4.5、node v6.2.1にはV8 v5.0が搭載されている。メジャーバージョンアップで大きな最適化が施されていることがわかる。さすがGoogleである。未だJSエンジンの進化は止まらず。

なおこのコードをFirefoxChromeの最新版で比較してみると…

Chrome 51.0.2704.79

bind: 127.187ms
closure: 34.819ms

Firefox 47.0

bind: タイマー開始
bind: 36.4ms
closure: タイマー開始
closure: 6.85ms

Firefox圧勝!

generatorとpromiseで行う非同期処理

JavaScript

2年ぐらい前にgeneratorでコールバック地獄から解放されるぜウェーイと話題になりましたが、今現在、世間ではgeneratorによる非同期処理ってどのくらい使われているんでしょうね? 自分たちのチームでは今更過去のコードを書き直すのも面倒なので使ってません。個人的にはasync/awaitがES.nextに入りそうなので今から使わなくてもいいかなぁ、という感じ。

generatorを使った書き方、よく忘れるのでここにメモしておく

まず適当なプロミスを返す関数を定義

'use strict';

// 引数を2倍して解決するpromiseを返す
function twice(value) {
  return Promise.resolve(value * 2);
}

// 引数ミリ秒後に解決するpromiseを返す
function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(() => {resolve(time)}, time);
  });
}

// 失敗するpromiseを返す
function error(e) {
  return Promise.reject(new Error(e));
}

promiseをyieldして非同期処理を実行するexecAsyncを定義、おそらくcoモジュールもこんな感じの処理をしているんでしょうね。

// promiseをyieldして非同期処理を実行するexecAsyncを定義
function execAsync(generator) {
  const g = generator();

  let n = g.next();   // ここで最初のyieldまで実行される

  if (!n.done) {         // yieldが無かった場合はn.doneはtrueになる
    loop(n.value);     // n.valueは最初のyield式のpromise
  }

  function loop(p) {
    p.then((result) => {
      let n = g.next(result);   // nextの引数はyield式の評価結果として返される。そして次のyieldまで実行
      if (!n.done) {
        loop(n.value);
      }
    }).catch((e) => {
      let n = g.throw(e);
      if (!n.done) {
        loop(n.value);
      }
    });
  }
}

実行する

// 実行!
execAsync(function *() {
  let a;
  a = yield twice(1);
  console.log(a);   // => 2

  a = yield twice(2);
  console.log(a);   // => 4

  const t = yield sleep(1000);
  console.log(`${t}ミリ秒停止した`);

  try {
    yield error('Promiseがrejectされると');
  } catch (e) {
    console.log(`${e.message}エラーがスローされる`);
  }

  a = yield twice(3);
  console.log(a);    // => 6

  a = yield twice(4);
  console.log(a);    // => 8
});

結果

2
4
1000ミリ秒停止した
Promiseがrejectされるとエラーがスローされる
6
8

obj && obj.hoge && obj.hoge.fuga ... を少し簡単に書く

JavaScript

Qiitaにも投稿してみた


JSerの皆様につきましては、JSON.parseしたデータなど、多段ネストしたオブジェクトのプロパティにアクセスするとき、

const obj = {hoge: {fuga: {piyo: 1}}};

const value = obj && obj.hoge && obj.hoge.fuga && obj.hoge.fuga.piyo || 0;

のようなコードを書いてウンザリしてることと思われます。どうやって回避してますか?

回避策1

const value = propOf(obj, 'hoge', 'fuga', 'piyo') || 0;
function propOf(obj, var_args) {
  let result = obj;
  const args = Array.prototype.slice.call(arguments, 1);

  while (args.length > 0) {
    const p = args.shift();
    result = result[p];

    if (result === null || result === undefined) {
      return null;
    }
  }
    
  return result;
}

回避策2

アロー関数が使える今なら、こういう書き方でもいいんじゃないかと思い始めた。

const value = safe(() => obj.hoge.fuga.piyo) || 0;
function safe(f) {
  try {
    return f();
  } catch (e) {
    return null;
  }
}

俺ならこう書く、というアイディアがあれば教えてください。

関数の可変長な仮引数を直接コンストラクタに渡す

node.js JavaScript

ある関数が受け取る可変長引数を、コンストラクタに直接渡す方法

要するにコンストラクタが可変長引数を受け取るのでファクトリ関数も可変長引数を使えるようにしたいんですよ。

ES2015ならばこう。

'use strict';
class Hoge {
  constructor(...args) {
    console.log(`引数は${args.length}個`);
  }
}

function createHoge(...args) {
  return new Hoge(...args);
}

createHoge(1);
createHoge(1, 2)
createHoge(1, 2, 3);

で、本題は次、スプレッド演算子とレストパラメータが使えない場合。LTSのnode.js v4系等ではどうするか?

普通の関数ならapplyでいい。

'use strict';
function hoge(var_args) {
  console.log(`引数は${arguments.length}個`);
}

function execHoge(var_args) {
  var args = Array.from(arguments);
  hoge.apply(null, args);
}

execHoge(1);
execHoge(1, 2)
execHoge(1, 2, 3);

しかし相手がコンストラクタの場合、new Hoge.apply(null, args) とするとapplyはコンストラクタじゃねーぞと言われてしまう。

ここは bind を使って頑張れば実現できそうな感じです。

'use strict';
class Hoge {
  constructor(var_args) {
    console.log(`引数は${arguments.length}個`);
  }
}

function createHoge(var_args) {
  var args = Array.from(arguments);
  args.unshift(null);
  return new (Function.prototype.bind.apply(Hoge, args))();
}

createHoge(1);
createHoge(1, 2)
createHoge(1, 2, 3);

bind.apply(Hoge, args)とするのがポイント、bindに対してapplyを実行することで引数を束縛したHogeコンストラクタを生成できるのだ。

非常にわかりにくいですねー。以上、今後全く役立たない技でしたー。

ES2015以前の可変長配列の書き方

JavaScript node.js

LTSのnode 4系ではRest Parametersは使えんのですよね。

'use strict';

function x(a, var_args) {
  const args = Array.prototype.slice.call(arguments, 1);
  
  console.log(a);
  console.log(args);
}

x('a');
x('b', 1);
x('c', 1, 2);
x('d', 1, 2, 3);

可変長の仮引数部分はvar_argsと書いておくのが流儀らしい。

実行結果

a
[]
b
[ 1 ]
c
[ 1, 2 ]
d
[ 1, 2, 3 ]

高速な同一の文字列結合

クソではないと言ったが、同一の文字列連結に関してはもっと高速なアルゴリズムがある。

たとえばxからxxxxxxxxという文字列を生成するなら

x + x

xx + xx 

xxxx + xxxx

普通にループすると8回の結合が必要だが3回で済む。このアルゴリズムを適用したパディングのコードを書くと以下のようになる。

'use strict';

function leftpad4(str, len, ch) {
    str = String(str);

    if (!ch && ch !== 0) ch = ' ';
    ch = String(ch);

    var max = len - str.length;
    for (var i = ch.length; i < max; i = i << 1) {
        ch += ch;
    }
    return ch.slice(0, max) + str;
}

では速度を比較してみよう。

結果

node v4.3.1, MacOSX 10.9.5, 2.5GHz Core i7, Memory 16GB DDR3

padding len 4 8 16 32 64 128
leftpag1 concat with + 12 77 166 294 544 1005
leftpad2 repeat and substring 68 92 179 210 276 401
leftpad4 fast concat algorism 31 56 165 202 251 395

パディングする桁が大きくなるほどleftpad1と比較してleftpad4がより高速になり、String.prototype.repeatを使ったバージョンと似た傾向を示した。実はrepeatはこのアルゴリズムを使っているので同じような処理速度となる。

ところで、パディングについてはさらに高速な手法もあったりするのだが…。ま、ヒマな人は考えてみてください。

left-padの文字列連結はクソではない

JavaScript

ここから始まる一連の、モジュールの依存性に関する議論はなかなか興味深いが、自分的に気になったのは以下の一節

GCを虐めるためとかコンパイラの最適化を確認するために用意する、「無駄に一時オブジェクト量産するクソコードの典型例」みたいな実装

ソースを見てみようか。

left-pad/index.js at 0e04eb4da3a99003c01392a55fa2fdb99db17641 · azer/left-pad · GitHub

なるほど一見するとクソコードにみえる。確かに、過去のJS処理系では str = ch + str のような文字列連結を大量に行うコードは非常に遅かった。このため多数の文字列を連結する場合、Arrayに結合する文字列をpushし、最後にjoinするという方法で高速化を図っていた。

しかし、それはv8登場以前の話である。今では文字列連結は処理系によって強烈な最適化を施され、Arrayを使うよりも高速に動作するようになっている。内部的には string-builder によって無駄なオブジェクトを生成する事なく文字列を構築するので、現在のJSでは一目でクソと言うほどクソではないはずだ。

と思ったのでベンチとってみた。処理系はnode v4.3.1

  • leftpad1、例のnpmモジュールのコード
  • leftpad2、現状ES.next proporsalのString.prototype.repeatで一気にpad文字列を作るコード
  • leftpad3、arrayに詰めてjoinするコード。ただしpushするのではなくES6のArray.fillで一気にpad文字列を作成するようにした(whileループでpushするよりも高速)

結果

padding len 4 8 16 32 64 128
leftpag1 concat with + 13 80 194 328 607 1138
leftpad2 repeat and substring 92 103 202 228 305 463
leftpad3 array fill and join 16 517 677 1012 1683 3007

数値は処理に要した時間、単位はミリ秒

  • Arrayでjoinは今となっては遅い。
  • lenが16以下の場合は+で接続する方が速い
  • lenが16より大きい場合はrepeatで一気にpad文字列を作るのが有利
  • len = 4のとき、leftpad1/3はほとんどの場合でstrをそのまま返すのでleftpad2より圧倒的に高速

結論

  • シンプルで高速なrepeatを使おう!

テストコード

gist.github.com