asm.jsの基本的な使い方・まとめ

written by DEFGHI1977.

本文書はasm.jsについてその使い方についてまとめたものです. 通常asm.jsスクリプトは手で記述することを前提としていませんが, 簡単な記法を憶えるだけで処理速度の向上が見込めるのはやはり魅力的です. 昨今はasm.jsをサポートする環境がひと通り揃いましたから, その使い方くらい知っておきたいところです. が, その一方でどのように記述出来るのかについての資料に未だ乏しいのが実情です.

そこで, asm.jsスクリプトのサンプルコードを実際に書いてみて, どのようなコードがコンパイルエラーとならないのかについて調べてみました. コードは全て実際に動作しているものですから, 開発ツールを開き, コンソール上に正常にコンパイル出来た旨のメッセージが表示されていることを確認してください. 実は資料に乏しくて取っ付き難いだけで, さほど難しい代物ではなさ気です. なお, あくまでJavaScriptユーザーがasm.jsの使い勝手について調べてみたものであり, asm.js仕様そのものの解説を目的としていません. そのため内容にはいろいろ間違いがあるかもしれません(特にデータ型と演算子の部分). また, パフォーマンスについてはあまり期待しないほうがよいでしょう.

目次

更新履歴

  • 2016/05/18 新規作成
  • 2016/05/19 ヒープサイズの指定法等を追加, 数値型の部分を書き直し.
  • 2016/05/20 整数とリテラルの掛け算について記述を追加.
  • 2016/05/24 ヒープサイズの計算による算出法を追加. Workerを使ったサンプルを追加.
  • 2016/05/25 boolean値についての扱いについて記述を追加. return文についての補足を追加.
  • 2016/05/26 ヒープサイズ算出式を修正. boolean値の扱いについての記述を修正.
  • 2016/05/27 文字列の取り扱いについての記述を追加.
  • 2016/05/28 TextEncoder/TextDecoderを追加.
  • 2016/05/29 const, 数値文字列に関わる記述を追加.
  • 2016/05/30 asm.js内外での定数値の共有についての記述を追加.
  • 2016/05/31 asm.jsの動的生成の項を追加. asm.jsコードを手組みする際の注意点について記述を追加.
  • 2016/06/01 前提知識の項を見直し.

前提知識

asm.jsとは

asm.jsとはWEBブラウザベンダーのMozillaが提唱し, 同団体が提供するFirefoxにおいて実装されたJavaScriptの高速化技術です. その有効性が確かめられると他のブラウザベンダーもこれに追随し, 現在ChromeやEdgeなどのブラウザでも利用可能となっています.

asm.jsが生まれた背景

HTML5が世間一般に利用されはじめてWEBアプリケーションは高機能化の一途を辿っています. が, その一方でHTMLが本質的に抱えるパフォーマンスの問題が浮き彫りになっています. もともとWEBブラウザは短いHTML文書による情報の伝達を主目的としており, その上で動作するスクリプトも極めて限定的なものでした. つまり, JavaScriptはさっと書いて直ぐに動くことが売りであり, その動作パフォーマンスが問題となることはさほどなかったのです. しかしHTML5仕様の複雑化からも判るように, 昨今は音声・映像処理と言ったネイティブアプリケーション並みの機能が求められています. すると, もともとハイパフォーマンス向けに設計されていないJavaScriptの部分がどうしても動作速度的に見劣りするようになってしまったのです.

この問題は当初JavaScriptインタープリタの機能向上(JITコンパイル)やハードウェア環境の進化により自然に解決するものと見込まれていました. しかし現実にはPCと比べスペック的に非力なタブレット端末やスマートフォンの環境の台頭もあり, いよいよ放置することができなくなったのです.

JavaScriptはなぜ遅いのでしょうか? その原因の一つが動的型付けです. 人間からすると, 実装したい処理と直接関わらない型付けと言ったことを考えずに済むため一見ありがたいものです. しかしコンピュータ側から見ると, どのように処理するべきかその都度計算(型判定)する必要があります. 一方, 余程のことが無い限り変数はその使い途が限定されており, 数値を扱う変数は最後まで数値を格納していることがほとんどです. つまり, 予め数値として扱うことが判っているにも関わらず, 型判定という余計な処理を行わざるを得ないのです. これは多量の数値演算を伴う映像処理と言ったものに多大な影響を及ぼします.

その対処策として, まず型付き配列と呼ばれるバイナリデータを効率的に扱うためのオブジェクトが生まれました. が, それだけでは根本的な解決とはなりません. そこで「JavaScriptの文法と矛盾しない形で型の宣言を行うことは出来ないか?」というアイディアが生まれました. 静的な型付けができれば, インタープリタはより効率的な構文解析が行え, スクリプトの処理速度が向上するはずです.

こうしてasm.jsが生まれました. asm.jsはJavaScriptには本来ありえなかった数値型の概念を導入し, インタープリタが処理を行う際のヒントを与えることに成功しています.

asm.jsの特徴

以下にasm.jsの特徴についてまとめます

このようにasm.jsは既存のJavaScriptコードに取って代わる存在ではありません. asm.jsはもともと(C言語やC++等の)他言語から出力したJavaScriptを効率的に動作させるにはどうすればよいかという発想から生まれており, そのためのWEBブラウザ側の追加機能として発展してきました. このことからasm.jsコードは比較的(コンピュータ向けの)低水準な内容とならざるを得ず, コードとしての可読性・効率性が犠牲になっています. 従って本来中身を知らずとも良い代物なのですが, あえてその動作原理を理解しておけば, asm.jsを使って出来る事についての見通しが立てやすくなります.

手組みのasm.jsコードの特徴

上記はasm.jsの一般論であり, 盲目的にasm.jsを採用すべきと考えるのは早計です. 本文書が目的としているasm.jsを手で扱う場合は更に次の点を考慮すべきです.

このことから, asm.jsを「使う」「使わない」に加え, 「asm.jsの文法に則りながらも敢えてasm.jsを利用しない」という第3の選択肢が得られます. つまり, asm.jsをコーディング規約として利用し, 局所局所での最適化を図るのです. スクリプトコードを手作りする上で, JavaScriptが提供する便利なオブジェクト群を捨てるのは現実的ではありません. そのため, どの範囲までasm.jsを導入するのかと言ったさじ加減が重要となります.

asm.jsの動作環境

asm.jsは現在Firefox, Chrome, Edge, Opera, Safariの各ブラウザで動作します. 従って, ほとんどのブラウザ環境でasm.jsコードを高速に実行することが可能です. また, Node.JSなどの一部のJavaScript実行環境においてもasm.jsをサポートしています.

asm.jsの動作確認をする

asm.jsの動作をWEBブラウザのスクリーンから直接確認する方法はありません. ブラウザ組み込みの開発ツール(主にFirefox)で確認する事になります.

asm.jsの今後

このようにして生まれたasm.jsでしたが, 導入実績が増えるにつれ新たな問題が判ってきました. スクリプトの動作速度の向上に寄与したことは間違いないのですが, その対価としてスクリプトのロードまでに時間がかかってしまうのです. asm.jsコードはJavaScriptコードとの互換性を保つために全てテキストで記述されています. 一般に文字データは(圧縮を掛けたとしても)バイナリデータと比べサイズが肥大する傾向があります. そのため, とりわけ3D映像処理やゲームと言った巨大な演算を要するものにおいては, 機械的に出力したスクリプトが数十MB単位となることも珍しくありません. その結果データ通信にかかる時間, 構文解析にかかるコスト等が無視できないものとなってしまいました.

ではどうすればよいでしょう? asm.jsコードの送信はいわば都度ソースコードから実行可能ファイルを生成しているようなものですから, 予め動作が保証されているバイナリデータをプログラムコードとして配布できれば問題は解決します. 加えてバイナリデータであれば高い圧縮効率も期待できます. こうして現在検討されているものがWebAssemblyです. CやC++等の言語から専用のコンパイラを通してWEBブラウザで実行可能なバイナリプログラム(いわばコンパイル済みのasm.jsモジュール)を生成する試みで, WEBブラウザ側の対応が進めば有力な選択肢となるはずです.

するとasm.jsの存在意義が再び問われることになりますが, 既にJavaScriptインタープリタの動作速度向上と言った点で一定の役割を果たしていることから今後も重要な技術であり続けることは間違いありません. また, 個人的には小規模な手書きのスクリプトにおいても高速化が期待できると言った面で研究対象となりうると考えています. 従って手書きasm.jsという若干無謀にも思える試みを幾ばくかでもサポートできれば…これがこの文書作成の動機です.

asm.jsはいつ導入すべきか?

さて, asm.jsはWEBアプリケーションのパフォーマンスを劇的に向上させる可能性を秘めていることはわかりました. ではasm.jsはどのようなケースにおいて導入すべきでしょうか?

いずれにせよ, 無闇に依存すべきものではありません. いわばasm.jsはJavaScript界の黒魔術とも呼べるもので, ソースコードの可読性低下, つまり生産性を犠牲にしてでも処理速度の向上を見込むわけですから, やり過ぎは却って自らに負担を強いる事になります. asm.jsを採用せずともJavaScriptのAPIは年々進化しており, 適切にオブジェクト・メソッドを選択するだけでも十分なパフォーマンス改善が見られるものです. とは言え, ループカウンタの記述等, ちょっと変えるだけでも目覚ましい効果が得られるケースもあり, 今後asm.jsから生まれた新たなJavaScriptのコーディング規約が浸透していくことも十分に考えられます.

asm.jsコードの記述

asm.jsコードの構成

asm.jsコードの記述はそれほど難しくありません. 簡単な例を示します.

  1. asm.js関数宣言
    asm.js関数は一般のfunction定義と同じように行います. この関数がasm.jsモジュールを生成します. function名は何でも良く, 無名functionとして即時実行しても構いません. また引数の名称についても制限はなく, 処理の内容に応じて適宜省略することが出来ます.
  2. use asm宣言
    先頭で"use asm";を指定するとfunction全体がasm.jsコードとして解釈されます.
  3. インポート宣言
    使用する組み込み関数(Math.xxx)や呼び出したい外部関数をここにリストアップしていきます.
  4. 共有変数宣言
    関数共通に利用したい変数をここにリストアップします. なお, 演算子を使った初期化は出来ません.
  5. 関数定義
    処理を行うための関数を記述していきます.
  6. 外部への公開
    記述した関数のうち外部に公開したいものを連想配列形式にまとめ, returnします.
  7. asm.jsモジュールの呼び出し
    asm.jsモジュールを定義した関数を呼び出すと, 上の(6)で生成した関数セットが返されるので, その中身を実行します.

asm.jsコードは見た目上通常のfunction宣言と区別がなく, script要素のどこに記述しても構いませんが, その構成は厳密に守る必要があります.

asm.js関数の引数の意味

asm.js関数として定義したfunctionは最大3つの引数をとります. asm.jsモジュールを使う場合適宜引数を渡します. それらの役割は次のとおりです.

なお第一引数にコンテキストオブジェクトに似たオブジェクトは渡さないでください. JavaScript文法としては正しくとも, asm.jsとしてリンク失敗となり通常のJavaScriptコードとしてフォールバック実行される可能性があります. .

"use asm"宣言

function定義の先頭に"use asm";と記述すると, JavaScriptインタープリタはこのfunctionをasm.jsコードと解釈し, 事前コンパイルを試みます. なお, 先頭行以外に記述した場合, コンパイルエラーとなりfunctionはフォールバック実行されます.

インポート宣言

asm.jsモジュール内部で利用する関数を宣言していきます. 主にfunctionの第一, 第二引数で指定したオブジェクトから必要な関数を変数にコピーします. なお, 第一引数からコピーできる関数には制限があり, stdin.Mathに属する数値関数(の一部)である必要があります. なお, asm.jsモジュールはfunctionスコープ外の変数にアクセスすることは出来ません. 従って, windowオブジェクトなどのグローバル変数を直接参照することは出来ません. またMath関数であってもインポートできないものがあります. なお, var宣言の代わりにconstを用いることも出来ます.

頻繁にインポートされるMath関数

Math関数のうち, 次の2つは特別な役割が与えられています.

この他stdin.Math.floorなどが型変換によく用いられます. 従って, asm.jsコードのテンプレートを作成する場合はこれらの関数をインポートするように記述しておくとよいでしょう.

共有変数・定数宣言

内部で共有する変数(var)や定数(const)を定義します. なお, 変数の初期化には数値リテラルの指定のみが行え, 演算子や制御構文を活用した複雑な値の指定はできません. またheapに対する型付き配列(ArrayBufferView)の生成は共有変数宣言部でのみ許されています. なお詳しい初期化処理が必要な場合は別途初期化用の関数を定義します.

ArrayBufferViewとして利用できるものは[Int/Uint][8/16/32]ArrayもしくはFloat[32/64]Arrayのいずれかです. Uint8ClampedArray, DataViewオブジェクトを指定することは出来ません.

関数定義

asm.jsモジュールが内部で実行するfunctionを定義します. 引数を受けるfunction場合, 引数を省略することは出来ません. また引数と戻り値については明示的に型を指定する必要があります. つまり, 引数の内容に応じてfunctionの処理の内容を変化させる(override/overloadする)ことは出来ません.

ローカル変数を定義することも出来ます. この場合, 共有変数の定義を同じ記法で行います(但しconstによる定数宣言は利用できません). また, 変数の宣言は具体的な処理の前に行う必要があります(変数定義の巻き上げ解釈は為されません). なお, 使えるものは数値のみでありそれ以外は組み込みオブジェクトであっても使えません.

functionから別のfunctionを呼び出すことも出来ます. 但しこの場合, パラメータを渡す側と受ける側の変数の型, 数が一致している必要があります.

関数内で利用できない構文

functionのネスト(functionをfunction内部で定義)は出来ません. これは関数内部でFunctionオブジェクトを利用できないのと同義です.

この場合, サブ処理も独立した関数として定義します.

モジュールの初期化処理の定義

asm.jsモジュールの初期化が必要な場合は, init関数を定義しその中で初期化処理を記述します.

ヒープ領域のサイズ指定

処理に最低限必要となるヒープ領域のサイズが判っている場合は, 次のように記述することでasm.jsモジュール生成時にheap領域のサイズが足りなかった際にメッセージを表示させることが出来ます.

この操作を行うfunctionを実際に呼び出す必要はありません. 定義しておくだけで有効になります.

外部への公開

asm.jsモジュールの生成処理が完了したら, その内容をオブジェクトにまとめreturnします.

asm.jsモジュールの呼び出し

asm.js関数を実行すると, asm.jsモジュールが生成されます. モジュールを使う側からは一般のfunctionオブジェクトと同じように扱えます. 但しその構成上パラメータとしては数値(及びそれに準じたもの)しか正しい内容を渡せません.

heap領域を使う場合は, asm.jsモジュール呼び出し側でArrayBufferオブジェクトを生成し, その内容をasm.js関数に渡します. 以後, asm.jsモジュールとはこのArrayBufferを介して数値データの受け渡しを行います.

heap領域を介したデータの授受

ヒープサイズの算出

前もって確保すべきヒープ領域のサイズがわからない場合は, 渡すデータが64kBの2n倍に収まるような0以上の最小のnを求めます.

asm.jsを実装する場所

asm.js関数を定義する場所は原則どこでも構いません. しかし, 工夫することでより使い勝手の良いものとなります.

asm.jsモジュールを非同期的に読み込む

asm.jsを使ったコードを手書きする場合はそれほど気になりませんが, 機械的に出力したものを同期的に読み込むことはWEBページの応答性を著しく阻害します. この場合, スクリプトを非同期的に読み込むことを検討します.

スクリプトを非同期的に読み込むにはscript要素を生成しDOMに挿入します.

Workerを使ってasm.jsモジュールを別スレッドで実行する

asm.jsモジュールにはその役割上重い処理をさせがちですが, このような処理はWorkerを用いてブラウザが動作しているメイン処理と切り離すようにするとよいでしょう. 簡単な例を示します.

Worker内部の処理はメイン処理とは別のスレッドで動作するため, messageイベントを介して非同期的にデータをやりとりします.

asm.jsコードの実装例

以下は画像にフィルタ効果(セピア化)をかける処理をasm.jsで実装したものです.

元画像 処理結果
フィルタ効果を掛けたcanvas要素

canvas要素にはグラフィックを描くためのAPIが備わっており, その中のgetImageDataを実行すると画像のピクセルデータを型付き配列(Uint8ClampedArray)オブジェクトとして取得できます. 従って, この内容をasm.jsモジュール管理下のheap領域に転写し, 処理を行った後でcanvas要素に書き戻せば画像の加工処理が完了したことになります.

asm.jsが扱う型

JavaScriptにおける整数表現, 単精度浮動小数点数表現

JavaScriptでは数値を表面上倍精度(64bit)浮動小数点数として扱います. が, 数値演算の内容によっては内部的に整数や単精度(32bit)浮動小数点数として扱うことも出来るようになっています. 内部的に整数/単精度浮動小数点数として扱える範囲であれば足し算であれ掛け算であれ演算結果は整数/単精度浮動小数点数となり, それを超えると自動的に倍精度浮動小数点形式に変換(暗黙的なキャスト)されるのです.

また, 組み込みの演算子・関数の特徴として次のものがあります.

すると, これらの仕組みを巧みに操り, 数値変数の内部形式を固定する(つまり擬似的に型付けされている)ようにコードを記述することが可能です. asm.jsではこのように記述されたコードを元に, キャスト処理を意図的に省いた実行コードを生成することで処理を高速化します. また, こうすることでJavaScript固有の動作が取り除かれ, LLVM中間コードからJavaScriptコードへの変換が可能となります.

変数と数値型

asm.jsは「(32bit)整数型:int」「倍精度(64bit)浮動小数点型:double」「単精度(32bit)浮動小数点型:float」の3つの数値型のみを扱います. string, またその他の組み込みオブジェクトは扱えません. なお, true/falseを表すboolean値はboolean型の代わりにint型を用います.

一度これらの型で扱うと決めた変数には最後まで同じ型の数値のみを格納します. もし, この条件に違反するとコンパイルエラーとして扱われ, 通常のJavaScriptコードとして実行されます.

型の宣言方法

変数に対する型を宣言場所としては「functionのパラメータの型の指定」「共有/ローカル変数の型の指定」「functionの戻り値の型の指定」の3つがあり, 型ごとに記述する方法が異なります.

変数の数値型の宣言記法
数値型functionパラメータ共有/ローカル変数function戻り値
inta = a|0;var a = 0;
var a = 0x0f;
return a|0;
doublea = +a;var a = 0.0;return +a;
floata = fround(a);var a = fround(0);return fround(a);

つまり変数に対する初回代入時の数値型をもって変数の型としているわけです. 従って変数のみを定義することは出来ません.

式内における型の補正

演算子や関数を伴う計算結果は元の数値型とは異なる型を返すことがあります. 例えば32bit整数の足し算が32bit整数の範囲を超えてしまうケースです. asm.jsではこのような数値を扱えませんから, 計算直後に|0を適用することで数値を32bit整数値に切り詰めねばなりません. 同様にdouble値, float値についても処理を継続するには演算結果が得られる度に数値型を補正するようにします.

数値型の補正法
数値型記法使用例
int([expression]|0)(values[0]|0)
double(+[expression])(+values[0])
floatfround(expression)fround(values[0])

この作業は変数を関数にパラメータとして渡す際, 関数から値を受け取った際, 型付き配列にインデックスを渡す際, 型付き配列の内容を受け取る際, 演算子に数値を渡す際のほとんどで行います.

数値リテラルの型

変数の型と同様に数値リテラルについても型が決定されます. これは先ほどの共有/ローカル変数の型指定にも使われていました.

数値リテラルの型表現
数値型記法備考
int0
0x0f
小数点が存在しないことで判定される. 16進記法も可能.
但し, 32bitの範囲(232-1)の数値のみ有効.
double0.0小数点が存在することで判定される. 2e-4と言った記法は使用不可.
floatfround(0)fround関数を被せることで表現する.

型付き配列

asm.jsはもうひとつ, 型付き配列のみアクセスすることが出来ます. これは外部から渡されたArrayBufferを汎用のメモリ領域として扱うためのもので, 用途に応じて型付き配列のインターフェースを被せて中身にアクセスします(ArrayBufferの中身を直接操作する手段はありません). 但しインデックスによる値の操作以外はコンパイルエラーとなります.

型付き配列では種類ごとに得られる数値が異なります. そのためasm.js内外でデータを共有する場合は, 大抵同じ型付き配列インターフェースが利用されます.

型付き配列と数値型の対応
型付き配列名要素毎の型値範囲対応する数値型1indexあたりのbyte数
Int8Array符号付き8bit整数-24〜24-1int1byte
Uint8Array符号なし8bit整数0〜28-1
Int16Array符号付き16bit整数-28〜28-12byte
Uint16Array符号なし16bit整数0〜216-1
Int32Array符号付き32bit整数-216〜216-14byte
Uint32Array符号なし32bit整数0〜232-1
Float32Array単精度浮動小数点数仮数23bit/指数8bitfloat
Float64Array倍精度浮動小数点数仮数52bit/指数11bitdouble8byte
型付き配列とbyte対応

型付き配列の内容を取り出す

型付き配列の内容を変数に取り出す際もしくは計算に用いる際, 型の補正が必要となります.

型付き配列では範囲外のインデックスにアクセスしようとした場合, JavaScriptではundefinedが返されます. 従ってそのままでは数値として扱えません. ここで型の補正を施すとそれぞれ0(undefined|0),NaN(+undefined), NaN(fround(undefined))となり, 型付き変数に格納可能となるのです.

型の補正は配列インデックスに変数を指定する際にも必要です. インデックスにはint型を渡す必要があるため, 適宜int型に補正します.

補足)型付き配列とエンディアン

JavaScriptでは通常メモリ内部でのバイトデータの並べ方(エンディアン)を考える必要はありませんが, 型付き配列を扱う場合は少し違います. ArrayBufferオブジェクトはJavaScriptが動作している環境の記憶領域に直接マップされているため, それを参照する型付き配列もエンディアンの影響を受けるからです. そのため, 複数のシステム間で生のバイナリデータを受け渡す場合はJavaScriptと言えどエンディアンを意識した処理が必要となります. JavaScriptではそのためにDataViewインターフェースが定義されています.

型の相互変換

asm.jsにおいては常に変数の型を意識しなければなりません. 従って, 異なる型の変数間で数値をやりとりする場合, 適切に型変換を指定しないとコンパイルエラーとなります. 以下に相互変換のための代表的なコードを示します.

型の相互変換
from\tointdoublefloat
int-+(a|0)fround(a|0)
double~~floor(a)-fround(a)
float~~floor(+a)+a-

int型に変換する必要がある場合は, Math.floor関数をインポートしておきましょう.

補足)型付き変数指定の副次的効果

変数に対する型付けは原則asm.js環境下でしか有効ではありません. がJavaScriptエンジンによっては通常のJavaScript動作環境においてもこの記法を元にコードの最適化を行うものがあります. 例えばループ処理を記述する場合, インデックス値のインクリメントに「i++」を使うよりも「i = (i+1)|0」を用いたほうがパフォーマンス的に有利な場合があります.

補足)asm.js仕様が定義している型

以下はasm.jsが仕様が定義している“概念としての”データ型です. 一部は一般的なプログラム言語における数値型の分類とも合致しており, 見方を変えると数値変数の内部状態とも言えます. 灰色のものは扱いに注意を要する型で, 処理を継続する場合には何らかの型の補正が必要となります.

asm.jsが定義している型
系統型名意味用途
特殊void無値であることを表す. 関数が何も返さないことをvoid値を返すと呼ぶ. -
externJavaScriptコードに渡す値を表す. foreign関数に渡す数値.
整数型intishJavaScriptの整数演算が返す値を表す. 演算結果が32bit整数値の範囲から外れている可能性があるため, 値の切り詰めを行う必要がある. 32bit整数のように見える値の意味. (1)int型に対する整数演算結果全般.
(2)型付き配列から得た数値.
int(符号の有無が不明な)整数を表す. asm.jsでは符号の有無(先頭bitの扱い)を意識せずに整数値演算する. よって確定値を持たないため, そのままでは外部に渡すことが出来ない. また, そのままでは比較出来ない. なので, 一旦signed型に変換する必要がある.(こうしても比較結果は変わらない)(1)var a = 0; a = a|0;等の変数定義時にこの型として判定される.
(2)比較演算子から得られた結果. (真:1, 偽:0)としてbit演算が可能.
signed符号付き整数(先頭bitを負値として解釈する)を表す. JavaScriptコードに渡すことが可能. a|0で得られた値. 配列のインデックス.
unsigned符号なし整数を表す. >>>演算子による処理結果.
fixnum0〜231-1の範囲の整数を表す. 先頭bitの意味合いに関わらず値が確定しているため"fix"numと呼んでいる. JavaScriptコードに渡すことが可能. 整数値リテラル等.
倍精度double?値が存在するかわからないdouble値を表す. 型付き配列に範囲外のインデックスを指定した場合のundefinedの可能性を含めている. 型付き配列から得た数値.
double倍精度浮動小数点数を表す. JavaScriptコードに渡すことが可能. a = +a;, double値に対する演算結果等.
単精度floatishJavaScriptにおける単精度浮動小数点数に対する演算結果を表す. 内容によっては倍精度浮動小数点数にアップキャストされている可能性があるため, 丸めを行う必要がある. float値のように見える値の意味. float型に対する演算結果全般.
float?値が存在するか判らないfloat値を表す. 型付き配列から得た数値.
float単精度浮動小数点数を表す. a = fround(a);fround関数で得られた値.

型をその特性で分類すると次のようになります.

型の分類
系統型名値は確定している?精度範囲内か?extern?
整数型intish未確定不明NO
int確実
unsigned確定
signedYES
fixnum
倍精度double?未確定NO
double確定YES
単精度floatish未確定不明NO
float?確実
float確定

演算子と関数

asm.jsで利用可能な演算子

asm.jsでは数値演算に関わる演算子のみが利用できます. 従って「||」や「&&」等の論理演算子や, 「new」や「instanceof」, 「===/!==」等のオブジェクトに関わる演算子はコンパイルエラーとなります.

大抵の演算子については左項と右項の型を揃える必要があります. また, 整数値に対する演算結果は32bit範囲から外れている可能性があるため切り詰め処理を要します.

利用可能な演算子
単項演算子+単項加算演算子doubleへの型変換.
-符号反転演算子演算後, 型補正の必要あり.
~ビット補数演算子整数値に適用する. 演算結果は整数値となる.
!論理補数演算子整数値に適用する. 比較演算子による演算結果の否定を表す.
二項演算子+加法演算子型を揃える必要がある.
-減法演算子型を揃える必要がある.
*乗法演算子型を揃える必要がある. int同士の掛け算には使えない.
/除法演算子型を揃える必要がある.
%剰余演算子型を揃える必要がある.
|,&,^,<<,>>ビット演算子演算結果は整数値となる.
>>>右シフト演算子演算結果は符号なし整数値になる.
<,<=,>,>=,==,!=比較演算子群型を揃える必要がある. 整数値(真:1/偽:0)が返る.
三項演算子a?b:c条件演算子型を揃える.

int型の掛け算

この内注意すべき演算子としては, 「*」が挙げられます. この演算子はint型の掛け算には使えません. 別途Math.imul関数を使う必要があります. なぜならJavaScriptにおける巨大な整数値同士の掛け算処理は容易に32bit整数の範囲を超えてしまうからです. そのため, 他の処理系との互換性を高めたMath.imul関数を使う必要があるのです.

なお, int型と整数値リテラルの掛け算であれば条件付きで利用できます. リテラル値が-220+1〜220-1の範囲であれば, いかなる32bit整数値と掛け算を行っても, double値における整数表現の範囲に収まる(つまり, 浮動小数点形式へのアップキャストが発生しない)からです. もちろん計算結果は32bit整数値の範囲から外れている可能性がありますから型の補正を行います.

比較演算子

==」等の比較演算子を用いる場合, 左項と右項の型を揃える必要があります. また, int型の場合はそのままでは符号の有無を判断することが出来ないため, 型の補正を行います. また比較結果を変数に格納する場合は, int型の変数に代入します.

比較結果を組み合わせる場合は論理演算子ではなく, ビット演算子を用います.

booleanリテラルの扱い

asm.jsでは真偽値リテラル「true/false」を使うことは出来ません. 別途整数値変数として定義することも出来ますが, 予約語である「true/false」とは別の名称を付ける必要があったり, 数値型としての比較を行わざるを得ず, 決して使い勝手の良いものではありません. どうしても必要な場合は「1/0」で代用しましょう.

条件演算子

条件演算子を用いる場合, then項とelse項の型を揃えます.

asm.jsで利用可能な算術関数・定数

演算子と同様にasm.jsでは数値演算に関わる算術関数及びそれに関わる数値定数のみが利用できます. いずれもMathオブジェクトのものを事前にインポートしてから利用します. なお, 戻り値の型が元と異なるものになっている可能性があるため, 適宜型の補正が必要となります.

利用可能な算術関数
関数acos, asin, atan, cos, sin, tan, exp, log引数(double)→戻り値double
ceil, floor, sqrt引数(double)→戻り値double
引数(float)→戻り値float
abs引数(int)→戻り値int
引数(double)→戻り値double
引数(float)→戻り値float
min, max引数(int,int,…)→戻り値int
引数(double,double,…)→戻り値double
atan2, pow引数(double,double)→戻り値double
imul引数(int,int)→戻り値intint型の掛け算に用いる
fround引数→戻り値floatfloat型への変換に用いる
利用可能な定数
定数Infinity, NaNdouble特殊な値
E, LN10, LN2, LOG2E, LOG10E, PI, SQRT1_2, SQRTdouble各種数学定数

一方Mathに定義されていてもインポート出来ないものもあります. roundsqrt等がこれに当たります. なお処理速度を犠牲にしても良いのであれば, foreign関数として利用することは出来ます.

構文

asm.jsと制御構文

asm.js環境においても原始的な制御構文は一部制限付きで動作します.

asm.jsで使える構文, 使えない構文
使えるもの使えないもの
if, switch, for, do-while, while, [label]:, break, continuefor-in, try-catch-finally, throw

条件判定文

if構文

if構文を使う際に用いる比較演算子は左項と右項の型を揃えておく必要があります.

switch構文

switch文の条件式はint型である必要があります. また, ラベルは数値リテラルでなければなりません.

ループ構文

for構文

for文でカウンタをインクリメントするには面倒ですが数値の代入を用います.

do-while, while構文, continue文, ラベル付きbreak文

asm.jsにおいてもdo-while及びwhile構文, continue文, ラベル付きbreak文は使用可能です.

制御構文とreturn文

asm.jsでは関数が返す値はどのような経路を辿ったとしても同じ型であることが求められます. これは, 実装ロジック上ありえないルートにおいても同様で, return文が存在しない場合にコンパイルエラーとなります. 例えば次のようなケースです. 通常switch構文の全てでreturn文を使っていた場合, そこまでで処理は終了しますが, asm.jsコンパイラのためにswitch文を抜けた先にもreturn文が必要となります.

asm.jsで使えない構文

逆に機能上の制限から使えない構文もあります.

つまり, 必要最低限の制御構文だけを使ってコードを記述していきます. これはいわゆる低級言語によるプログラミングに似ています. 実際, asm.jsコードの元となるLLVM形式の中間コードは, CやC++等の高級言語で記述されたプログラムをより機械語寄りに変換したものです. これらは本来プログラムのしやすさと言ったものとは無縁のものであり, 結果asm.jsコードも似たようなものになります.

外部連携

JavaScript側のfunctionを呼び出す

asm.js関数の第二引数foreignにfunctionオブジェクトを設定しておくとasm.jsモジュールからモジュール外部の任意の関数(foreign関数)を呼び出すことが出来ます. この機能を用いると, asm.jsの機能だけでは実現できない仕組みをJavaScriptコードで補うことが可能になります. 例えば, ログの出力やUIコンポーネント(input要素)の設定値と言った内容をasm.js内部から参照することが可能になります. 但し, その仕組み上受け渡し可能なデータは数値に限られますし, 処理を動作速度の遅い世界に任せることになりますから使いすぎは禁物です.

foreign関数を介した処理の連携

foreign関数の構成

foreign関数を定義する場合, 次の点に注意します.

また, foreign関数を使う(asm.js)側では次の点に注意します.

パラメータの数によって処理を変化させる

foreign関数はJavaScriptの世界で定義されています. つまり, その実装内容に制限はありません. 従って次のようにパラメータ数不定の関数を定義しても, asm.js側から正しく呼び出せます.

foreign関数を介した外界へのデータ引き渡し

foreign関数を使えば, asm.jsの世界から外部にデータをpushすることが出来ます. これは例えばログの出力に応用できるでしょう.

foreign関数を介してasm.jsにデータを渡す

foreign関数のreturn値を用いるとasm.jsモジュールから外部の任意の数値を取得することが可能です. 但しその特性上取得できるのは単一の値のみです.

asm.jsモジュールを連携させる

foreign関数としてasm.js関数が生成した関数を指定することも出来ます.

文字列とasm.js

数値文字列をasm.jsに渡す

数値が文字列として表現されていた場合, その文字列をasm.jsモジュールに渡すとその仕組み上自動的に数値型に変換されます. 変換ルールは|演算子, +演算子, Math.fround関数の(つまり, JavaScript標準の)仕様に従います.

文字列をasm.jsに渡す

asm.jsはもともと数値計算に特化した環境ですから, 一般的な文字列に対する処理は行えません. そもそもJavaScript本体が高度な文字列処理機構を備えているため, そのような処理を改めて実装する必要は無いはずです. しかし処理を最適化する目的から独自に文字列処理を実装する場合は, 文字列を数値データに変換することでasm.jsモジュール内部に渡します.

文字列から文字コード値への変換にはString.prototype.charCodeAtを, 逆に文字コードから文字への変換にはString.fromCharCodeを用います. この操作を文字列の長さの分だけ繰り返すのです. なお, 得られる値はUnicodeでの値(0〜65535・16bit値)になります.

TextEncoder/TextDecoderによるバイナリデータ化

文字列をバイナリデータに変換するAPIとして, 最も扱いやすいものとしては以下に示すTextEncoder/TextDecoderが挙げられます. これは文字列データを直接Uint8Arrayに変換するため, asm.jsモジュールと非常に親和性の高いものになっています.

Blobを経由した文字列のバイナリデータ化

Blob(Binary Large OBject)は本来ローカル環境のデータを扱う場合に便利なAPIですが, 直接文字列をバイナリデータ化する際にも利用できます.

文字列を一旦Blobオブジェクトに変換し, それをFileReadeでArrayBufferに変換した後asm.jsモジュールに渡すようにします. 処理結果はまたBlobオブジェクトとし, FileReaderで文字列に戻します. なお, Blobの読み込みをloadendイベントなどを使った非同期処理として記述せねばならず, コードの記述が若干煩雑化します. また, Blobを介した文字列処理では文字コードが強制的にutf-8として扱われるため, データの取り扱いがやや面倒です. 従ってBlob単独での利用は考慮しないほうが良いかもしれません.

Worker内部でのBlob処理

Worker環境下ではFileReaderオブジェクトの代わりにFileReaderSyncが利用できます. FileReaderと異なり, Blobオブジェクトの読み込みを(イベントを介さず)同期的に行えるため, 処理の流れに沿ったコードの記述が可能です.

asm.js関数の動的生成

ソースコードからasm.js関数を生成する

asm.jsモジュールを生成するasm.js関数も元を正せば単なるfunctionに過ぎません. 従って, eval関数やFunctionオブジェクトによる動的なasm.js関数の定義が可能です. 例を示します. function内部の処理は通常複数行で記述することが多いため, `を使ったテンプレート文字列によるコード記述が便利です.

これを応用すると, ソースコードの一部を外部から取得(インクルード)すると言った仕組みを構築することが可能です.

外部設定のインクルード

テンプレート文字列において${[変数名]}と記述することで外部変数の中身を文字列内部に取り込むことが出来ます. これを応用すると様々な設定を共有可能となります.

asm.js内外で定数を共有する

処理の都合上asm.jsモジュール内外で定数値を共有したい場合, 初期化関数を経由して定数値をasm.js内部に伝播するようにしますが, いささか記述が面倒です.

この時ソースコード文字列からasm.js関数を生成する方法を用いると, 簡潔に記述できます. asm.js内で定数を定義している部分をパラメータとし, 外部定数で置き換えるのです.

関数のインポート設定を共有する

複数のasm.jsモジュールを生成する場合, 参照しているMath関数の一覧を別変数に取り出しておくことで設定を共有することが出来ます.

関数定義を共有する

同様に共通に利用する関数そのものを別変数に切り出すことも可能です.

よくあるエラーと対処

asm.jsコンパイル時に発生するエラーのうち, 代表的なものについてその対処策についてまとめています. メッセージはFirefoxでのものです.

エラーメッセージ原因と対処策
TypeError: asm.js type error: Disabled by debugger 開発ツールのデバッガが有効となっている. 開発ツールを一旦閉じ, ページをリロード後もう一度開発ツールを起動する.
TypeError: asm.js link error: ArrayBuffer byteLength of 0x10000 is less than 0x6000000 (the size implied by const heap accesses). ヒープ領域(ArrayBuffer)のサイズが足りない. 十分なサイズのArrayBufferを渡すようにする.
TypeError: asm.js type error: Math not found in module global scope Mathオブジェクトの所属を明示しなかった.
誤)var imul = Math.imul;
正)var imul = stdin.Math.imul;
TypeError: asm.js type error: Uint8Array not found in module global scope コンストラクタの所属を明示しなかった.
誤)var values = new Uint8Array(heap);
正)var values = new stdin.Uint8Array(heap);
TypeError: asm.js type error: unsupported import expression 共有変数宣言時に演算子などを使ってしまった.
var a = 1 + 1;
初期化処理は別途init関数を用意する.
TypeError: asm.js type error: one arg to int multiply must be a small (-2^20, 2^20) int literal int値の掛け算を行おうとした. Math.imul関数で書き換える.
誤)var a = 0; a = a * a;
正)var a = 0; a = imul(a, a);
TypeError: asm.js type error: intish is not a subtype of int (1)整数値の変数をそのまま使おうとした. 配列のインデックス指定時に頻出. 型を補正する.
誤)var a = 0; values[a+1];
正)var a = 0; values[(a+1)|0];
(2)配列の中身をそのまま使おうとした. 型を補正する.
誤)var a = 0; a = values[0];
正)var a = 0; a = values[0]|0;
TypeError: asm.js type error: unsupported expression asm.jsで利用できない演算子を利用した. 利用可能な記法で置き換える.
誤)var a = 0; a++;
正)var a = 0; a = (a+1)|0;
TypeError: asm.js type error: arguments to a comparison must both be signed, unsigned, floats or doubles; int and int are given 比較演算子の左辺と右辺とが比較不能である, もしくは型が異なる. 型を補正するかキャストして型を揃える.
誤)var a = 0; a == a;
正)var a = 0; (a|0) == (a|0);
TypeError: asm.js type error: incompatible number of arguments (1 here vs. 0 before) function定義時の引数の数とfunction呼び出し時の引数の数が合っていない. 引数を明示する.
TypeError: asm.js type error: unexpected statement kind asm.jsで使えない構文を使おうとした. 同等の処理を行える構文に書き換える.
TypeError: asm.js type error: int is not a subtype of extern foreign関数呼び出し時に変数の型の補正をし忘れた. 変数の型を補正する.
TypeError: asm.js type error: void incompatible with previous return type 関数が返す型が場所によって異なる. (1)returnの箇所を一箇所にまとめる. (2)関数の最後にreturn文を追加する.

参考文献