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

abcdefGets

ゲッツ!

jspmからWebpackに移行した

とりあえず、Webpackを導入したがそのままだと色々問題が多かったので以下の事をやった。
まあ今更感あるが。

  • typescriptのallowSyntheticDefaultImportsをfalseにする。
  • production用とdev用のconfigをいい感じにわける
  • node_modulesのdllを生成する。

typescriptのallowSyntheticDefaultImportsをfalse

今回はtypescriptとwebpackの組み合わせだったのだが、webapckに移行する前にはjspmを利用していたので、

allowSyntheticDefaultImports: true,
module: system,

で運用していた。ちなみにこのallowSyntheticDefaultImportsが何かというと、
本来typescriptのES6 Moduleはexport default ...の構文しか、直接importできない。

// a.js
module.exports = function() {}
// b.ts
import fn from './a'; // Error
import * as fn from './a'; // OK

しかし、allowSyntheticDefaultImportstrueにすると、commonjsスタイルのmoduleも直接importできるようになる。
のだが、webpackはsystemjsを取り扱えず、module: commonjsにしたところ、このallowSyntheticDefaultImportsがうまく動作しなくなった。

// a.js
module.exports = function() {}
// b.ts
import fn from './a';

fn(); // Error

ので、allowSyntheticDefaultImportsfalseにして、すべてimport * as ...の方式で読み込むことにした。

production用とdev用のconfigをいい感じにわける

まあ、webpack.config.js自体がnodejsのモジュールだったので、webpack.config.jsに設定を書いてもいいのだが、
凄くごちゃごちゃするので、webpack.dev.config.jsを作って、そこでwebpack.config.jsを読み込み、
_.cloneで設定をコピーしてdev用の設定を追加するようにした。

node_modulesのdllを生成する。

コンパイルがあまりに遅かったので、DllPluginを利用して、変更されない外部モジュールを全てまとめておいた。

dll側

output: {
  filename: '[name].dll.js',
  library: 'vendor_library' // Bundleが定義されるグローバル変数名。参照する時に使用する。
},
plugins: [
  new webpack.DllPlugin({
    path: path.join(__dirname, 'dll', '[name]-manifest.json'), // manifestファイルを出力する。とりあえず-manifestにしとけばok
    name: 'vendor_library' // output.libraryで指定した名前
  })
]

使う側

plugins: [
  ...
  new webpack.DllReferencePlugin({ // Dllを参照する
    context: __dirname,
    manifest: require('./dll/vendor.production-manifest.json') // 上のmanifestファイルへのパス
  })
]

こうすることでnode_modulesとかのあまり変化しないファイルをコンパイルしておくことができ、
そのコンパイルしたファイルを参照することでコンパイル時間を短縮できる。
ただし、DllReferencePluginはconcatしてくれないので、dllは手動でconcatするか、別途scriptで読み込む必要がある。
今回はconcatした。

gulp.task('minify', done => {
  const webpack = require('webpack');
  const config = _.clone(require('./webpack.config.js'));
  config.output = _.clone(config.output);
  config.output.path = path.join(__dirname, DIST);
  const compiler = webpack(config);
  compiler.run((err, stats) => {
    if (err) {
      throw err;
    }
    console.log(stats.toString('minimal'));
    try {
      const main = `${DIST}/main.js`;
      const app = fs.readFileSync(main, 'utf8');
      const vendor = fs.readFileSync(`dll/vendor.production.dll.js`, 'utf8');
      fs.writeFileSync(main, `!function(){${vendor};\n${app}}();`, 'utf8');  //ここでconcatしてる。
    } catch(e) {
      throw e;
    }
    done();
  });
});

さらに新たにnode_modulesがロードされても更新されるように、package.jsonにも以下の記述をした。

"scripts": {
  "postinstall": "node ./node_modules/.bin/gulp bundle-dll"
}

これでnpm install/yarn addしてもbundleが更新される。

あと、はまった点として、予めbundleしておく中にreactとかも入れていたので、process.env.NODE_ENVの値に困ってしまった。
なので、dllファイルをdev用、production用と2つ用意して対処した。

function bundleDll(env, done) {
  const webpack = require('webpack');
  const config = _.clone(require('./webpack.dll.config.js'));
  config.entry = _.clone(config.entry);
  config.output = _.clone(config.output);
  config.output.path = path.join(__dirname, 'dll');
  config.plugins = config.plugins.slice();

  const vendor = config.entry.vendor;
  const keyName = `vendor.${env}`;
  config.entry = {};
  config.entry[keyName] = vendor;
  
  if (env === 'production') {
    config.plugins.push(new webpack.optimize.UglifyJsPlugin());
  }
  config.plugins.push(new webpack.DefinePlugin({
    'process.env.NODE_ENV': `'${env}'`
  }));
  const compiler = webpack(config);
  compiler.run((err, stats) => {
    if (err) {
      throw err;
    }
    console.log(stats.toString('minimal'));
    done();
  });
}

/**
 * dev dll
 */
gulp.task('bundle-dev-dll', done => {
  bundleDll('development', done);
});


/**
 * production dll
 */
gulp.task('bundle-prod-dll', done => {
  bundleDll('production', done);
});


gulp.task('bundle-dll', () => {
  const rs = require('run-sequence');
  return rs(
    'clean-dll',
    'bundle-dev-dll',
    'bundle-prod-dll'
  );
});

まとめ

jspmよりwebpackの方が快適でした。
さよならjspm

V8 Iginition Interpreter

以前、東京Node学園25時限目で発表した内容を修正して書いていこうと思う。

というわけで、V8にバイトコードインタープリタ Ignition が搭載された。
このインタープリタは単純そうに見えて非常にわかりづらいので解説していく。

バイトコードインタープリタとは

インタープリタとは、ソースコードを逐次実行する形式のエンジン。
今までのV8はソースコードを即アセンブラコンパイルし実行していたが、
インタープリタはそれとは違い、一度ソースコードを高レベルなバイト命令に変換し、そのバイト命令を逐次実行していく。
高級アセンブラみたいな感じ。

Ignition概要

Ignitionはレジスタベースのバイトコードインタープリタである。Javaのスタックベースとは違って、CPUのレジスタに実際に値を割り付けて実行する。

IgnitionはBytecodeHandlerと呼ばれるバイトコード処理関数を予め生成しておき、バイトコードから配列のインデックスを取得、
そのインデックスに生成された処理関数を割り当て、Bytecodeの配列を次々巡回して、対応するインデックスの関数を呼び出しコードを実行する。

JSで非常に単純化されたコードを書くと以下の様になる。

var Bytecodes = [0,1,2,3,4,5];
var index = 0;
function dispatch(next) {BytecodeHandlers[next]();}
const BytecodeHandlers = {
  ['0']() {...; dispatch(Bytecodes[index++])},
  ['1']() {...; dispatch(Bytecodes[index++])},
  ['2']() {...; dispatch(Bytecodes[index++])},
  ['3']() {...; dispatch(Bytecodes[index++])},
  ['4']() {...; dispatch(Bytecodes[index++])},
  ['5']() {...; dispatch(Bytecodes[index++])},
}

このモデルを念頭にV8のコードベースを確認していく。

Ignitionの構造

バイトコード生成までの道のり

IgnitionはJavascript ASTからバイトコードを生成する。
このバイトコード生成のステップを確認していく。

BytecodeGeneratorAstVisitorを実装しているので、Javascript ASTを巡回しながら対応しているバイトコードを生成していく。 BytecodeGeneratorsrc/interpreter/bytecode-generator.hにあり、バイトコード生成メソッドはBytecodeGenerator::GenerateBytecodeである。

さて、BytecodeGenerator::GenerateBytecodeはどこから呼ばれるかというと、InterpreterCompilationJob::ExecuteJobImpl(src/interpreter/interpreter.cc)内で呼び出される。
InterpreterCompilationJob::ExecuteJobImplstatic Interpreter::NewCompilationJobで実行される。

Interpreter::NewCompilationJobの階層は以下のようになっている。

Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

このstatic Interpreter::NewCompilationJobコンパイラパイプラインのジョブを生成するメソッドなので、compiler.cc(src/compiler.cc)を見ていこう。

compiler.cc(src/compiler.cc)は非常に複雑でわかりづらい呼び出し階層をもっており、さらにオプションの設定パーサーの設定も相まって非常に読みづらい。
static Interpreter::NewCompilationJobを呼び出すまでのコールスタックは以下の様になっている。

ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
Interpreter::NewCompilationJob
  |
  FullCodeGenerator::NewCompilationJob

ScriptCompiler::CompileがV8のJavascript Compilerのエントリーポイントとなっており、そこから順次関数を呼び出し、最終的にInterpreterのJobを生成する。

最終的なBytecodeGenerator::GenerateBytecodeまでの呼び出しコールスタックは以下のようになる。

ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
| FullCodeGenerator::NewCompilationJob
|
Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

バイトコード生成

さて、呼び出し階層を把握したところで、バイトコードの生成方法を見ていく。
バイトコード生成は先程も書いたとおりAstVisitorを継承しているので、各種Visit***メソッドを実装する必要がある。
ので、各種Visit***の実装を見ていけば何をしているか理解できるはず。

ただ、闇雲にコードを見てもバイトコード自体は理解できないので、一旦trace_bytecodeでd8を実行してみる。

javascript

var a = 1;

bytecodes

0  [generating bytecode for function: ]
1  Parameter count 1
2  Frame size 32
3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4           0x3f5e20aafdf8 @    2 : 1f f9             Star r1
5           0x3f5e20aafdfa @    4 : 02                LdaZero
6           0x3f5e20aafdfb @    5 : 1f f8             Star r2
7           0x3f5e20aafdfd @    7 : 20 fe f7          Mov <closure>, r3
8           0x3f5e20aafe00 @   10 : 55 aa 01 f9 03    CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9      0 E> 0x3f5e20aafe05 @   15 : 92                StackCheck
10   116 S> 0x3f5e20aafe06 @   16 : 09 01             LdaConstant [1]
11          0x3f5e20aafe08 @   18 : 1f f9             Star r1
12          0x3f5e20aafe0a @   20 : 02                LdaZero
13          0x3f5e20aafe0b @   21 : 1f f8             Star r2
14          0x3f5e20aafe0d @   23 : 03 01             LdaSmi [1]
15          0x3f5e20aafe0f @   25 : 1f f7             Star r3
16          0x3f5e20aafe11 @   27 : 55 ab 01 f9 03    CallRuntime [InitializeVarGlobal], r1-r3
17          0x3f5e20aafe16 @   32 : 04                LdaUndefined
18   118 S> 0x3f5e20aafe17 @   33 : 96                Return
19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21  - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22  - length: 2
23            0: 0x3f5e20aafd71 <FixedArray[4]>
24            1: 0x2315b1a87ef9 <String[1]: a>

そうするとこのような結果が得られる。

さてバイトコードを出力したのはいいが、見方がわからないと意味が無いので、見方も解説。

ここは関数のbytecodeの場合に関数名が入る。今回はグローバルなので空。

0  [generating bytecode for function: ]

これはstackのパラメータの数。
今回のバイトコードはグローバルなので無視。

1  Parameter count 1

FrameSizeは割り当てたレジスタの数 * ポインタのサイズ
ポインタのサイズは大体の環境で、32bitでは4byte、64bitでは8byteになる。
今回の場合、割り当てたレジスタ数の数が4 64bit環境なので、ポインタサイズが8byte
4 * 8 = 32となる。

2  Frame size 32

各バイト列は 現在のアドレス アドレスのオフセット バイトコードの数値 バイトコードの名前 オペランド となっている。

3  0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]

ここは定数値プールの中身。
今回は変数名のaがプールされている。

19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21  - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22  - length: 2
23            0: 0x3f5e20aafd71 <FixedArray[4]>
24            1: 0x2315b1a87ef9 <String[1]: a>

さてこれらの情報を踏まえて、先程のソースコードバイトコードを見ていこう。

以下の部分はすっとばしてよい。ここはインタープリタの準備なので。

3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4           0x3f5e20aafdf8 @    2 : 1f f9             Star r1
5           0x3f5e20aafdfa @    4 : 02                LdaZero
6           0x3f5e20aafdfb @    5 : 1f f8             Star r2
7           0x3f5e20aafdfd @    7 : 20 fe f7          Mov <closure>, r3
8           0x3f5e20aafe00 @   10 : 55 aa 01 f9 03    CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9      0 E> 0x3f5e20aafe05 @   15 : 92                StackCheck

本番はここから
解説はコード中に書いていく。

            // 定数プールのインデックス1(変数名a)から値をaccumulatorにロードする。
10   116 S> 0x3f5e20aafe06 @   16 : 09 01             LdaConstant [1]
11          // accumulator(変数名a)からr1レジスタに値をロードする。
12          0x3f5e20aafe08 @   18 : 1f f9             Star r1
13          // accumulatorに0をロードする。
14          0x3f5e20aafe0a @   20 : 02                LdaZero
15          // accumulator(0)からr2レジスタに値をロードする。
16          0x3f5e20aafe0b @   21 : 1f f8             Star r2
17          // accumulatorに即値1をロードする。
18          0x3f5e20aafe0d @   23 : 03 01             LdaSmi [1]
19          // accumulator(1)からr3レジスタに値をロードする。
20          0x3f5e20aafe0f @   25 : 1f f7             Star r3
21          // r1レジスタからr3レジスタの値(a, 0, 1)を使ってInitializeVarGlobalランタイムを呼び出す。
22          0x3f5e20aafe11 @   27 : 55 ab 01 f9 03    CallRuntime [InitializeVarGlobal], r1-r3
23          // accumulatorにundefinedをセット
24          0x3f5e20aafe16 @   32 : 04                LdaUndefined
25          // 終了
26   118 S> 0x3f5e20aafe17 @   33 : 96                Return

これがバイトコードの実行である。
ちなみにCallRuntimeの場合、各Runtime毎に呼び出し規約が決まっているので、それぞれに合わせたレジスタの割り当てが必要になる。
InitializeVarGlobalランタイム呼び出しは以下のレジスタを期待している。

  • r0 = 束縛される変数名
  • r1 = LaunguageMode SLOPPY(通常) STRICT(strictモード) LAUNGUAGE_END(不明)
  • r2 = 束縛される値

そのため、上記のコードは

  • accumulatorに値をロード
  • レジスタに値をロード

を繰り返して、Runtime呼び出しのコードを生成している。

とこの調子でIgnitionはバイトコードを実行していくが、
そのバイトコードを実行しているのはBytecodeHandlerとよばれるクラスである。

バイトコード実行

BytecodeHandler

バイトコードの実行はBytecodeHandlerによって行われる。
このBytecodeHandlerはV8の初期化時に生成され、配置される。

以下がBytecodeHandlerの例である。

IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
  Node* zero_value = NumberConstant(0.0);
  SetAccumulator(zero_value);
  Dispatch();
}

LadZeroの処理を行うBytecoeHandlerで、中では単純にaccumulatorに0をセットするだけ。
このような調子で各バイトコードにつき一つのBytecodeHandlerが実装されている。

BytecodeHandlerは直接次のBytecodeHandlerを呼び出す。

このDispatchが次のBytecodeHandlerを呼び出している。

Dispatch();

しかし、このBytecodeHandlerの実装をみるとわかるのだが、BytecodeHandlerはあくまで、
実行予定Nodeを組み立てているだけで、実際には何かを実行するわけではない。

Ignitionインタープリタは最初にBytecodeの処理手順をグラフノードで生成し、生成したグラフからマシンコードを生成する。
これをBytecodeのdispatch-tableに設定することで、各バイトコード毎に行う処理が設定されたBytecodeHandlerが実装される。

以下の図はBytecodeHandlerの生成

f:id:brn_take:20170501171341p:plain

InterpreterEntryTrampoline

Ignitionは最終的にBytecodeArrayを生成し終わった後に、
InterpreterEntryTrampolineというbuiltinsからIgnitionのDispatchTableを発火するコード生成し、
BytecodeArrayからバイトコードを取り出し、対応するDispatchTableの処理を実行して回っていく。

以下の図はIgnitionが実行される様子

f:id:brn_take:20170501171955p:plain

まとめ

一通りIgnitionの実行パスを眺めた。
また、Ignitionのバイトコードアセンブラコードのキーとして振る舞い、
実際にはベースラインで生成されたコードが実行されている事を確認した。

TurboFan経由の最適化部分等については今後の記事を書く予定。

typescript 2.3 RC

typescript 2.3 rcがアナウンスされた

主な変更点は以下の通り

–strictオプションの追加

以下の型チェックオプションを有効にする

  • –noImplicitAny
  • –strictNullChecks
  • –noImplicitThis
  • –alwaysStrict

以下の様に部分的にOFFにもできる

{
    "compilerOptions": {
        "strict": true,
        "noImplicitThis": false
    }
}

generateor、iteratorのES3、ES5対応

--downlevelIterationフラグをONにすることで、
generatorとiteratorがES3、ES5共にトランスパイルできるようになった。

Async generators & iterators

ES Proposalのasync iteratorとasync generatorに対応した。

async iteratorの構文

for await (let item of items) {
    /*...*/
}

async generatorの構文

async function* asyncGenName() {
    /*...*/
}

ただし、Async generatorとAsync iteratorを使うためには、 Symbol.asyncIteratorが必要なので、以下のようにして、polyfilを作る必要がある。

(Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol.from("Symbol.asyncIterator");

(Symbol as any).asyncIterator = Symbol.asyncIterator || "__@@asyncIterator__";

まとめ

遂に(async) generator、iteratorがES3、ES5でも使えるようになってよかったね。

gulp-uglifyでプロパティ名をmangleする

全然ドキュメントがなかったので備忘録。

ClosureCompilerみたいにプロパティ名もmangleしたい。
こんなの

const x = {
  doSomething() {return ...}
  doNothing() {}
}
x.doSomething();
x.doNothing();
const x = {
  a() {return ...}
  b() {}
}

x.a();
x.b();

設定

gulp.task('minify', () => {
  const uglify = require('gulp-uglify');
  gulp.src(['src/index.js'])
    .pipe(uglify({
      mangle: true,
      compress: true,
      mangleProperties: {
        ignore_quoted: true
      }
    }));
});

これが基本。

で、特定のプロパティ名のリネームを防ぎたい場合は、reservedという機能を使う

gulpfile.js

gulp.task('minify', () => {
  const uglify = require('gulp-uglify');
  const Uglify     = require('uglify-js');

  let reserved = Uglify.readReservedFile('./reserved.json');
  reserved = Uglify.readDefaultReservedFile(reserved);

  gulp.src(['src/index.js'])
    .pipe(uglify({
      mangle: true,
      compress: true,
      mangleProperties: {
        reserved: reserved.props,
        ignore_quoted: true
      }
    }));
});

reserved.json

{
  "vars": [  ],
  "props": [ "doSomething", "doNothing" ]
}

これでdoSomethingdoNothingはmangleされなくなる。

上記の記述の

let reserved = Uglify.readReservedFile('./reserved.json');
reserved = Uglify.readDefaultReservedFile(reserved);

の部分ではUglifyjs2が必要なので、別途npm installしてくだはい。
後は必要なプロパティ名をガンガンpropsに突っ込んでいけばOK。

ただ、ClosureCompilerもそうだけど、プロパティ名のmangleにはそれなりのリスクがあるので、
コンパイル後にも統合テストをしたほうが良い。

まとめ

gulp-uglifyが不親切でつらい。

ES6のComputedPropertyNameとトランスパイラ

ES6のComputed Property Nameは非常に便利だが、トランスパイラを併用すると問題が起きがちである。
それを確認していく。

Base

const SYMBOL = Symbol('foo-bar-baz');

const obj = {
    [SYMBOL]: 1,
    name: 'brn',
    job: 'engineer'
}

typescript

var SYMBOL = Symbol('foo-bar-baz');
var obj = (_a = {},
    _a[SYMBOL] = 1,
    _a.name = 'brn',
    _a.job = 'engineer',
    _a);
var _a;

babel

'use strict';

var _obj;

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var SYMBOL = Symbol('foo-bar-baz');

var obj = (_obj = {}, _defineProperty(_obj, SYMBOL, 1), _defineProperty(_obj, 'name', 'brn'), _defineProperty(_obj, 'job', 'engineer'), _obj);

これを見るとすぐわかるが、トランスパイルされたjs内では、
オブジェクトを一度構築してからプロパティを追加している。
SYMBOLプロパティは仕方ないが、他のプロパティも別途追加になっているため、(Babelはもっとひどくて、definePropertyになっている…)
例えば、v8ではDictionaryモードになってしまう。つまりパフォーマンスに重大な問題を引き起こす。
そのため、パフォーマンスクリティカルな場合はComputedPropertyの利用は避けよう。

Ecmascript decorator を使ってキャッシュする

表題の通り ES proposal のdecoratorをつかってメモ化のようなことをするライブラリを前に書いたので、 キレイにして、GitHubに公開した。

GitHub - brn/cache-decorator: javascript method/function cache decorator.

インストール

npm install cache-decorator --save

yarn add cache-decorator --save

使い方

メソッドのキャッシュ

For javascript/babel

import {
  cache,
  CacheType,
  CacheScope
} from 'cache-decorator';

class Example {
  @cache({type: CacheType.MEMO, scope: CacheScope.INSTANCE})
  expensiveCalc(args) {...}
}

For typescript

tsconfig.json

{
  "compilerOptions": {
    ...
    paths: {
      "cache-decorator": ["node_modules/cache-decorator/lib/index.d.ts"]
    }
  },
}
import {
  cache,
  CacheType,
  CacheScope
} from 'cache-decorator';

class Example {
  @cache({type: CacheType.MEMO, scope: CacheScope.INSTANCE})
  public expensiveCalc(args) {...}
}

関数のキャッシュ

For javascript/babel

import {
  fcache,
  CacheType
} from 'cache-decorator';

const cachedFn = fcache((args) => {
  ...
}, {type: CacheType.MEMO})

For typescript

tsconfig.json

{
  "compilerOptions": {
    ...
    paths: {
      "cache-decorator": ["node_modules/cache-decorator/lib/index.d.ts"]
    }
  },
}
import {
  cache,
  CacheType,
  CacheScope
} from 'cache-decorator';

const cachedFn = fcache((args: Object) => {
  ...
}, {type: CacheType.MEMO})

cacheのオプション引数

interface CacheOption {
  type?: CacheType;
  scope?: CacheScope;
  ttl?: number;
  compare?: (prev: any, next: any) => boolean;
}

type: CacheType

default value: CacheType.SINGLETON

SINGLETON

  • キャッシュを探す
  • もしキャッシュがあればその値を変えす。なければメソッドを実行して、その結果を保存する.
  • 結果を返す

MEMO

  • キャッシュを引数のリストとともに検索し、保存されている引数リストと比較する.
  • もし値が見つかれば結果を返し、見つからなければメソッドを実行し、結果と引数のペアをキャッシュに保存する。
  • 結果を返す

scope: CacheScope

default value: CacheScope.INSTANCE

INSTANCE

キャッシュされた値はインスタンス毎の領域に保存され、インスタンス間で値が共有されない。

GLOBAL

すべてのキャッシュはグローバルな領域で管理され、すべてのインスタンスで値が共有される。

ttl: number

default value: null

指定されたミリ秒でキャッシュを破棄する。

compare: Function

default value: (a, b) => a.length === b.length && a.every((v, i) => v === b[i])

CacheType.MEMOが指定された場合に引数を比較するのに使う比較関数。

まとめ

メモ化は結構面倒なので、decoratorというシンプルな形でまとめられてよかった。
使い勝手も悪くないので結構重宝している。

Function.prototype.bind のパフォーマンスについて

ふとパフォーマンスが気になったので調査した。
記憶が正しければ、callよりも遅いはず。

というわけでレッツ検証

事前準備

package.json

{
  "name": "bench",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "brn",
  "license": "MIT",
  "devDependencies": {
    "benchmark": "^2.1.3"
  }
}

bench.js

/**
 * @fileoverview
 * @author Taketoshi Aono
 */

const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;

const bind = (() => {}).bind({});
const bindWithArgs = ((a, b, c) => {}).bind({}, 1, 2, 3);
const call = () => {};
const callargs = {};
const callcall = () => call.call(callargs)
// add tests
suite
  .add('bind', () => {
    bind();
  })
  .add('bind with args', () => {
    bindWithArgs();
  })
  .add('call', () => {
    callcall();
  })
  .on('cycle', function(event) {
    console.log(String(event.target));
  })
  .on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ 'async': true });

node bench.js

結果は V8限定ですが,

bind x 43,404,436 ops/sec ±2.07% (83 runs sampled)
bind with args x 35,140,882 ops/sec ±2.48% (84 runs sampled)
call x 59,048,983 ops/sec ±1.13% (85 runs sampled)
Fastest is call

となりました。 予想通り、callが最速。

ただ、気になるのはbindに引数を束縛した場合さらに遅くなる点。

気になるので調べました。

V8のコミットID
bdf32cf1bc96982ff5a22195d874617ffae03e79
時点のものです。

コード検証

色々さがして、とりあえずx87のbuiltin-x87.ccを見る。
x64でもいいけど、とりあえずx87を調べましょう。

でこれが、bindで生成した関数を呼び出すアセンブラコード生成関数。

void Builtins::Generate_CallBoundFunctionImpl(MacroAssembler* masm,
                                              TailCallMode tail_call_mode) {
  // ----------- S t a t e -------------
  //  -- eax : the number of arguments (not including the receiver)
  //  -- edi : the function to call (checked to be a JSBoundFunction)
  // -----------------------------------
  __ AssertBoundFunction(edi);

  if (tail_call_mode == TailCallMode::kAllow) {
    PrepareForTailCall(masm, eax, ebx, ecx, edx);
  }

  // Patch the receiver to [[BoundThis]].
  __ mov(ebx, FieldOperand(edi, JSBoundFunction::kBoundThisOffset));
  __ mov(Operand(esp, eax, times_pointer_size, kPointerSize), ebx);

  // Push the [[BoundArguments]] onto the stack.
  Generate_PushBoundArguments(masm);

  // Call the [[BoundTargetFunction]] via the Call builtin.
  __ mov(edi, FieldOperand(edi, JSBoundFunction::kBoundTargetFunctionOffset));
  __ mov(ecx, Operand::StaticVariable(ExternalReference(
                  Builtins::kCall_ReceiverIsAny, masm->isolate())));
  __ lea(ecx, FieldOperand(ecx, Code::kHeaderSize));
  __ jmp(ecx);
}

ここで注目したいのが、
Generate_PushBoundArguments(masm);
の部分
束縛した引数をPushするコードだと想像できる。

Generate_PushBoundArgumentsがこちら。

void Generate_PushBoundArguments(MacroAssembler* masm) {
  // ----------- S t a t e -------------
  //  -- eax : the number of arguments (not including the receiver)
  //  -- edx : new.target (only in case of [[Construct]])
  //  -- edi : target (checked to be a JSBoundFunction)
  // -----------------------------------

  // Load [[BoundArguments]] into ecx and length of that into ebx.
  Label no_bound_arguments;
  __ mov(ecx, FieldOperand(edi, JSBoundFunction::kBoundArgumentsOffset));
  __ mov(ebx, FieldOperand(ecx, FixedArray::kLengthOffset));
  __ SmiUntag(ebx);
  __ test(ebx, ebx);
  __ j(zero, &no_bound_arguments);
  {
    // ----------- S t a t e -------------
    //  -- eax : the number of arguments (not including the receiver)
    //  -- edx : new.target (only in case of [[Construct]])
    //  -- edi : target (checked to be a JSBoundFunction)
    //  -- ecx : the [[BoundArguments]] (implemented as FixedArray)
    //  -- ebx : the number of [[BoundArguments]]
    // -----------------------------------

    // Reserve stack space for the [[BoundArguments]].
    {
      Label done;
      __ lea(ecx, Operand(ebx, times_pointer_size, 0));
      __ sub(esp, ecx);
      // Check the stack for overflow. We are not trying to catch interruptions
      // (i.e. debug break and preemption) here, so check the "real stack
      // limit".
      __ CompareRoot(esp, ecx, Heap::kRealStackLimitRootIndex);
      __ j(greater, &done, Label::kNear);  // Signed comparison.
      // Restore the stack pointer.
      __ lea(esp, Operand(esp, ebx, times_pointer_size, 0));
      {
        FrameScope scope(masm, StackFrame::MANUAL);
        __ EnterFrame(StackFrame::INTERNAL);
        __ CallRuntime(Runtime::kThrowStackOverflow);
      }
      __ bind(&done);
    }

    // Adjust effective number of arguments to include return address.
    __ inc(eax);

    // Relocate arguments and return address down the stack.
    {
      Label loop;
      __ Set(ecx, 0);
      __ lea(ebx, Operand(esp, ebx, times_pointer_size, 0));
      __ bind(&loop);
      __ fld_s(Operand(ebx, ecx, times_pointer_size, 0));
      __ fstp_s(Operand(esp, ecx, times_pointer_size, 0));
      __ inc(ecx);
      __ cmp(ecx, eax);
      __ j(less, &loop);
    }

    // Copy [[BoundArguments]] to the stack (below the arguments).
    {
      Label loop;
      __ mov(ecx, FieldOperand(edi, JSBoundFunction::kBoundArgumentsOffset));
      __ mov(ebx, FieldOperand(ecx, FixedArray::kLengthOffset));
      __ SmiUntag(ebx);
      __ bind(&loop);
      __ dec(ebx);
      __ fld_s(
          FieldOperand(ecx, ebx, times_pointer_size, FixedArray::kHeaderSize));
      __ fstp_s(Operand(esp, eax, times_pointer_size, 0));
      __ lea(eax, Operand(eax, 1));
      __ j(greater, &loop);
    }

    // Adjust effective number of arguments (eax contains the number of
    // arguments from the call plus return address plus the number of
    // [[BoundArguments]]), so we need to subtract one for the return address.
    __ dec(eax);
  }
  __ bind(&no_bound_arguments);
}

想像よりなが~いーーー

__ j(zero, &no_bound_arguments);で引数束縛がなければjmpするコードを生成

その後は、BoundArgumentsを保存するstack領域を確保、リターンアドレスをstackに押し込んで、
BoundArgumentsをstackに押し込む。
これをループで行っているのでだいぶ遅そう。

Function.prototype.bindが引数束縛つきで遅くなるのはこれが理由っぽい。

まとめ

  • 最速は() => fn.call(this)形式
  • 次点でfn.bind(this)
  • 引数の束縛はかなり遅くなるので、() => fn.call(this, ...)のがおすすめ。

アロー関数のおかげでFunction.prototype.bind使い所がない。