はじめに注意!
- この記事で扱う内容は現時点(2015-07-19)で実用的でない
- とりあえず触ってみた、遊んでみたというメモ
- うごくまでには色々弄る必要があります
なにをするの?
- ES6/ES7をES5/3にトランスパイルするBabelというツールがある
- Babelのプラグインとしてbabel-plugin-asm-jsというものが最近公開された
- Flowの型付けがされたES6をEmscriptenを使わずにasm.js化するもの
- Flowの型はTypeScriptの型と互換性がある
- …つまりTypeScriptのコードをこのツールにかければasm.jsが簡単に吐き出せるんじゃね?
- やってみよう
インストール
TypeScriptのインストール:
- Type Aliasを使うのでver1.4以降
- 今回コンパイルはしない
- 文法や型チェックにつかうだけなので
--noEmitオプションを有効にしておく
Babelのインストール:
npm i babel -D
babel-plugin-asm-jsのインストール:
npm i babel-plugin-asm-js -D- WARNINGが出るがとりあえず
使えるようにする
babel-plugin-asm-js は 現時点(2015-07-19)で npm installしただけでは動かない。なので手を入れる。
package.json
node_modules/babel-plugin-asm-js 以下の package.json
"main": "lib/index.js",
↓
"main": "dist/index.js",
存在しないパスを指定してあるのでプラグインが見つからないという事になる。
better-logのインストール
npm i babel-plugin-asm-jsでは入らないがエラー出力に必要なので入れる。
npm i better-logするか、node_modules/babel-plugin-asm-js 以下でnpm iで入るはず。
コード(TypeScript)
コードの書き方は色々制限がある。
以下がとりあえず動く最小コード。
"use asm";
type int = number;
type double = number;
type float = number;
export function sample(intParam:int, doubleParam:double, floatParam:float):int{
return 0;
}
use asmディレクティブ必須
"use asm";
- ファイルの最初に書いておかないと トランスパイルされない
type alias指定が必要
- asm.jsで指定できる3つの型を扱えるようにtype aliasを作っておく
type int = number;
type double = number;
type float = number;
- TypeScriptの場合、型定義ファイル(*.d.ts)に宣言を持っていかないほうがよい1
使いたい関数をexport
- ES6形式の
exportで関数を指定する -
export defaultはできない -
Classは未対応
引数と返り値に型指定
- 現時点では関数の返り値に
void指定できない - 現時点ではasm.jsの型への変換は 引数と返り値のみ
トランスパイル後(asm.js化)
では実際に変換してみよう。
CLIから.tsファイルを直接指定できる。
ファイルとして書き出す時は-oオプションで指定する。
babel --plugins asm-js sample.ts
トランスパイルすると以下のコードになる。なお、このコードはそのままでは動かない(後述)。
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = initialize;
var selfGlobal = _selfGlobal(),
foreign = {},
strings = [];
var _sample;
exports._sample = _sample;
function asm(stdlib, foreign, heap) {
"use asm";
function sample(intParam, doubleParam, floatParam) {
intParam = intParam | 0;
doubleParam = +doubleParam;
floatParam = Math.fround(floatParam);
return 0 | 0;
}
return {
sample: sample
};
}
function initialize(heap) {
var _asm;
return (_asm = asm(selfGlobal, foreign, heap), _sample = _asm.sample, _asm);
}
核となるコード
色々出力されるが、核となるのは以下の部分。asm.jsの書き方に即した形で出力される。
function asm(stdlib, foreign, heap) {
"use asm";
function sample(intParam, doubleParam, floatParam) {
intParam = intParam | 0;
doubleParam = +doubleParam;
floatParam = Math.fround(floatParam);
return 0 | 0;
}
return {
sample: sample
};
}
- asm.jsの型表現に変換されている(引数、返り値)
-
intは変数|0 -
doubleは+変数 -
floatはMath.fround(変数) - 関数内の 変数宣言 → 処理 → return文 という順番に入れ替えられる(asm.jsの仕様)
現時点では実用できない理由
変数:intとすると変数|0と置き換えてくれるなど、一瞬いいね!と思うのだが、残念ながらまだ実用できない。
普通の変数の型は変換してくれない
export function sample(intParam:int, doubleParam:double, floatParam:float):int{
let a:int = 100; //関数内でローカル変数を宣言してみると…?
return 0;
}
function sample(intParam, doubleParam, floatParam) {
intParam = intParam | 0;
doubleParam = +doubleParam;
floatParam = Math.fround(floatParam);
var _a = 0;
_a = 100;
//関数内でローカル変数を宣言してみると…?
return 0 | 0;
}
-
letも使えるし、ちゃんと変数宣言を最初に持ってきてくれるのはいい - が! 大事な型情報が消えてしまう。
-
var _a = _a|0;としてほしい - まあなくても一応エラーにはならないみたいだが
結局、asm.jsを手書きする必要がある
速度的に早くなっているかどうかを確認するため以下の関数を書いてみた。
export function doForLoop(from:int, to:int, add:int):int{
console.time("asm_forloop");
for(from=0;from < to; from += add){
}
console.timeEnd("asm_forloop");
return 0;
}
これはトランスパイル時に次のエラーになる。
TypeError: sample.ts: Line 26: Unsupported operation: int < int
asm.jsは厳格な型の世界なので、比較演算子<も左右の辺の型が対応していないといけない。
比較演算子が対応するのは仕様では以下になる。
(signed, signed) → int ∧
(unsigned, unsigned) → int ∧
(double, double) → int ∧
(float, float) → int
asm.jsのint型は signed/unsignedの区別のない32bit型integerである。比較するためには型のキャストが必要である。(int|0)とかint >>> 0とかで変換できるのだが、冗長でめんどくさい。
最初からsignedやunsignedで型指定すればいい、と思いたいのだがasm.jsにはそれを表現するイディオムがない。type aliasでuintとか定義しても現時点では無意味である。JavaScriptの文法上、演算子オーバーロードもないので結局以下のように書かないといけない。
for(from=0;(from|0) < (to|0); from += add){
変数宣言時の変数|0は区別なしの(int)として扱われるが, 比較時の両辺での変数|0は(signed)として扱われる。 同じ表記だが型が違う ので注意。
さらに、この修正後のコードもFirefox上のランタイムエラーになる。+=がアウトらしく、さらに次のように書き直す必要がある。
for(from=0;(from|0) < (to|0); from = from + add){
babel-plugin-asm-jsは内部でasm-jsのバリデータを使っているのだが、上記のエラーはバリデータでは判断できず、ランタイムで確認するしか無い。asm.jsでは現時点ではfirefoxとchromeで有効だが、chromeでは出ないエラーなので厄介。
なお、 Firefoxはデバッガを起動するとasm.jsが無効になるという仕様 があり、エラーチェックのためにデバッガを起動したままに出来ない(コンソールだけならOK)。
TypeScript(おそらくFlowも)の型チェックやBabel-asm-jsのトランスパイル時の型チェックだけでは現状、asm.jsの型の整合性を取る事は厳しい。asm.jsの仕様を理解した上で asm.jsを結局手書きする事になる。
吐き出されるコードがそのままでは動かない(2015-07-19)
出力されたコードは現時点(2015-07-19)で動かないものである。
ざっくりと以下のように手を入れれば一応動くようにはなる。
-
_selfGlobalを定義する -
"use strict";直後に以下のように定義する
var _selfGlobal = require("babel-runtime/helpers/self-global")["default"];
- babel-runtime も
npm iで導入しておく -
_selfGlobalは関数でないというエラーになるので以下の部分を書き換える - BEFORE:
var selfGlobal = _selfGlobal(), - AFTER:
var selfGlobal = _selfGlobal, - initializeするために一番最後の行に以下を追加。
exports["default"](0);- 引数はヒープサイズなので適宜変更しておく
- exports._FUNC = _FUNC; の部分をさらに一番最後の行に移動
- 外部から使いたい関数を登録している部分
- さらに
_を削る - BEFORE:
exports._FUNC = _FUNC; - AFTER:
exports.FUNC = _FUNC;
以上をやることで、node上で使えるコードになる。
var asm = require(`./sample.js`);
asm.doForLoop(0,100000,1); //0: 3ms
なお、ブラウザで使うためにはもう一手間を入れる必要がある。
まとめ
-
引数:intが変数|0に変換されるのは便利 - ただし、関数の引数と返り値だけ
- ES6に一部対応しているのも便利
- 結局、asm.jsを手書きする必要がある
- 吐かれるコードがそのままでは動かないので手を入れる必要がある
- まだまだあまり実用的じゃないので今後に期待
- asm.jsを手書きしてた人には便利?2
TypeScriptは処理速度に貢献しないので、TypeScriptでasm.jsを使えると、Cのインラインアセンブラみたいに部分的に早くできる!と思ってトライしてみたのだが、実用にはまだ難あり。あと、あまりTypeScript関係なかった。