TypeScriptとBabelでasm.jsに挑戦する

More than 3 years have passed since last update.


はじめに注意!


  • この記事で扱う内容は現時点(2015-07-19)で実用的でない

  • とりあえず触ってみた、遊んでみたというメモ

  • うごくまでには色々弄る必要があります


なにをするの?


  1. ES6/ES7をES5/3にトランスパイルするBabelというツールがある

  2. Babelのプラグインとしてbabel-plugin-asm-jsというものが最近公開された


  3. Flowの型付けがされたES6をEmscriptenを使わずにasm.js化するもの

  4. Flowの型はTypeScriptの型と互換性がある

  5. …つまりTypeScriptのコードをこのツールにかければasm.jsが簡単に吐き出せるんじゃね?

  6. やってみよう


インストール


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)

コードの書き方は色々制限がある。

以下がとりあえず動く最小コード。


sample.ts

"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ディレクティブ必須


1行目

"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

トランスパイルすると以下のコードになる。なお、このコードはそのままでは動かない(後述)。


babel-asm-js後

"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+変数


    • floatMath.fround(変数)



  • 関数内の 変数宣言 → 処理 → return文 という順番に入れ替えられる(asm.jsの仕様)


現時点では実用できない理由

変数:intとすると変数|0と置き換えてくれるなど、一瞬いいね!と思うのだが、残念ながらまだ実用できない。


普通の変数の型は変換してくれない


BEFORE(ローカル変数を宣言してみる)

export function sample(intParam:int, doubleParam:double, floatParam:float):int{

let a:int = 100; //関数内でローカル変数を宣言してみると…?
return 0;
}


AFTER(型が失われる)

    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とかで変換できるのだが、冗長でめんどくさい。

最初からsignedunsignedで型指定すればいい、と思いたいのだがasm.jsにはそれを表現するイディオムがない。type aliasでuintとか定義しても現時点では無意味である。JavaScriptの文法上、演算子オーバーロードもないので結局以下のように書かないといけない。


修正後

for(from=0;(from|0) < (to|0); from += add){


変数宣言時の変数|0は区別なしの(int)として扱われるが, 比較時の両辺での変数|0(signed)として扱われる。 同じ表記だが型が違う ので注意。

さらに、この修正後のコードもFirefox上のランタイムエラーになる。+=がアウトらしく、さらに次のように書き直す必要がある。


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関係なかった。





  1. TypeScript自体のチェックは通るが、babel-asm-jsで変換されるものがかわってしまう 



  2. そんな人どれだけ居るのだろうか…?このツールを使っても、C/C++で書いた方が生産的だと思う