kanejaku.org

Misreading chat #81 を聞いた

5 Oct 2020

Misreading chat が再開された。めでたい。

強連結成分とりだしてトポロジカルソート、とかいかにもアルゴリズムの授業でありそう、とか思って聞いていたら正にそういう感想を話されていた。

マイナーな SoC でもカーネルツリーからコンパイルすることなく動く perf 的な not-perf というプロファイラをちょっと前に使っていたのだけど、これどうやって動いているんだろう、後で調べようと思って結局調べていないことを思い出した。

追記 (2020-10-06): 森田さんに perf_event_open を使ってあとは自力で頑張っているっぽい、ということを教えてもらった。ありがとうございます。

プロファイラの話題、続編があるといいなあ。最近趣味プロジェクトでプロファイリングをしているので興味がある。

サイトのデザインを少し変更

3 Oct 2020

もうすっかり秋だ。外を歩いていると金木犀の香りが風に乗ってやってくる。爽やか。

観測範囲内でブログへの回帰が進んでいる。このサイトでももうちょっと気軽に、わりとどうでもいいことを日記風につけるのも悪くないかと思い始めた。これまでのトップページはエントリごとにリンクがあって、日記的に運用するのとは相性が悪い。ということで適当に Pagination して最新のエントリを表示するようにした。あとはアナリティクスの利用も明示するように。

ちなみにこのサイトは Hugo で生成して Netlify で運用している。最初は Github pages で運用していたが Netlify だとデプロイが簡単なので気に入っている。ロードはやや遅く感じるけど、速度は重要ではないから特に困ってはいない。

Minimp3 Wasm Without Emscripten

8 Feb 2020

I created a web-based mp3 decoder using minimp3. The decoder is a tiny WebAssembly compiled with clang version 9 (without Emscripten). Applying wasm-opt, the binary size of the decoder is just 21 KB.

Here is a demo page and the repository.

This article describes how I created the decoder.

minimp3 has very few dependencies. It just requires memcpy(3), memmove(3), and memset(3). We can easily find naive implementations of these functions on the Web.

An interesting part is memory allocation. To decode a mp3, we need to pass the mp3 data to minimp3 and allocate memory for decoded PCM data. Both require dynamic memory allocations.

One might think of using malloc(3). This is the way I took first, but after reading Surma’s article that describes wasm-ld’s memory layout, I came up an idea – having a fixed memory layout and allowing dynamic memory allocation only for input (MP3 data) and output (PCM data). This approach reduced most of the complexities of memory allocation. I no longer needed a generic malloc(3) implementation. That eliminated the need for Emscripten.

Here is the memory layout.

We put input MP3 data at the beginning of the heap (that is, __heap_base). The size of the input MP3 data is stored in mp3_data_size. Decoded PCM data follows it and its size is stored in pcm_data_size. Assuming that the Wasm is used from a single thread, the decoder information such as minimp3’s context (mp3dec_t), number of samples etc are placed on statically allocated memory.

This memory layout doesn’t allow us to free up input MP3 data after decoding. This is a trade-off not having a generic memory allocator. Practically this wouldn’t be a problem because we need to allocate memory for PCM data before decoding and WebAssembly doesn’t provide a way to shrink memory. If we want to free up input data, what we can do is to copy the decoded PCM data somewhere and discard the Wasm instance.

With this memory layout, decoding a MP3 data would look like the below:

// decoder.js

const { instance } = await WebAssembly.instantiate(...);
const wasm = instance.exports;
const mp3Data = /* Some Uint8Array */;

// Set MP3 data into Wasm memory.
wasm.set_mp3_data_size(mp3Data.byteLength);
const mp3DataInWasm = new Uint8Array(
  wasm.memory.buffer,
  wasm.mp3_data_offset(),
  wasm.mp3_data_size());
mp3DataInWasm.set(mp3Data);

// Decode.
wasm.decode();
const pcm = new Int16Array(
  wasm.memory.buffer,
  wasm.pcm_data_offset(),
  wasm.pcm_data_size() / 2);

// Use `pcm`.
// decoder.c

extern unsigned char __heap_base; // Defined by LLVM.

static size_t mp3_data_size;
static size_t pcm_data_size;

const uint8_t *mp3_data_offset() {
  return &__heap_base;
}

size_t mp3_data_size() {
  return mp3_data_size;
}

void set_mp3_data_size(size_t size) {
  mp3_data_size = size;
  // Grow memory if needed to set MP3 data.
}

const uint8_t *pcm_data_offset() {
  return mp3_data_offset() + mp3_data_size;
}

size_t pcm_data_size() {
  return pcm_data_size;
}

void decode() {
  pcm_data_size = /* Compute PCM size using minimp3 */;
  // Grow memory if needed for PCM data.

  const uint8_t *mp3 = mp3_data_offset();
  const int16_t *pcm = pcm_data_offset();
  // Decode `mp3` using minimp3 and put samples into `pcm`.
}

A tricky part is to use wasm-ld’s __heap_base. By specifying a linker option -Wl,--export-all I was able to use __heap_base in my code, but the option also exported functions which weren’t needed, bloating Wasm binary size slightly.

What I needed was -Wl,--export=__heap_base. --export is a lld’s option which isn’t specific to WebAssembly and that’s why it isn’t listed on the document of wasm-ld.

The remaining parts of the implementation aren’t specific to Wasm. The github repository has the full implementation.

Side notes

We don’t need a custom MP3 decoder if we don’t need to decode large MP3 files. BaseAudioContext.decodeAudioData() is enough for most MP3 files, but decodeAudioData() didn’t work well for my use case. I want to decode large MP3 files like ATP podcast. Decoding such MP3 files consumes a lot of memory which I want to avoid.

WebCodecs may eliminate the demand of this kind of decoder. This is one of APIs I’d like browsers to implement.

黄昏

6 Jan 2020

今の住処は見晴らしがよい。

カタカタとキーボードを叩いていると部屋が暗くなってきて、あ、もう夕方だ、と気づいて窓に視線を送る。夕陽はもうビルの向こう。紅色から蒼色へのグラデーションに魅了され、寒さが堪えるけれど窓を開ける。

冬の澄んだ空気に触れた。はっと子供のころに住んでいた家を思い出す。

雪は降らないけれど早朝には霜が降りる。安普請な家だったから冷え性の自分には堪えた。

近くには電車の駅があった。駅から聞こえてくる笛の音で目が覚めたのを思い出す。冬の時期には良く通る音だった。

身震いしつつそんなことを懐かしむ。

2019年の振り返り

30 Dec 2019

今年はアウトプットを重視した年だった。取り組み始めたら投げ出さずに区切りのいいところまでやる。ある程度達成できたと思う。その一方で集中して取り組まないと解けない問題は避けてしまった印象がある。


趣味プロジェクトを区切りのいいところまで終わらせる、そしてそれらを持続させることを意識した。機能したと思う。以下今年やれたこと。

  • 勉強がてら自分に特化した英語辞書アプリを Rust で書き直して普段使いできるレベルまで作った。
  • Chrome Mojo IDL の Language Server Protocolを実装した。
  • フォントフォーマット変換 Web App をメンテした。
  • NES エミュレータをマリオが動くくらいまでは実装した。

特に年末に自由に使える時間があったのが有難かった。やりたいと思っていたことを消化できた。

  • 仕事の優先度を考えると手を出しづらかったが余暇としてやれたらいいな、と思っていたことができた。
  • アドベントカレンダーに参加できた。
  • 私的に頼まれたツールを二つぐらい作れた。

公開しない前提で日記をつけるようにようにした。森田さんのやり方を真似させてもらった。日々の振り返りをする上でこれもいい感じに働いたと思う。その分、ブログを書く意欲がなくなってしまったが、これは妥当なトレードオフだと思う。余談: 森田さんの昨今の記録が公開されていてうれしい。

leetcode は 2, 3 問解いただけでほとんどやらなかった。試験対策に対する興味が失われた…というのは易いけど地頭を鍛えるのを怠ったというのが実情だろう。

英語について。昨年から英会話のレッスンに通ったことでだいぶん苦手意識がなくなった。コードを書いていてもコメントを書くのに労力がいらなくなったのは大きい。喫緊の案件ではなくなったせいで今年はさぼりがちであり、オフィス移転もあって後半はレッスンに通うのをやめてしまった。デザインドキュメントを書くのには相変わらず苦労しているので引き続き改善点のひとつではある。

やりたかったけど時間を割かなかった趣味もある。自分の優先度は「晩酌>>>>コーディング>(越えられない壁)>その他の趣味」 なので毎晩のように酩酊しつつコードを書いていると時間が溶けてしまう。ある程度しょうがないと割り切っているが来年は読書の時間を増やしたい。

今年聞き始めたポッドキャストで一番気に入ったのが IGN Japan のしゃべりすぎゲーマー。聞き始めのころは「なんだこのイキリオタクは…」とか思っていた今井さんという方がいるのだが、今ではすっかりファンになってしまって配信の時に居ないとちょっとがっかりしてしまう。他の語り手も本当に魅力的で毎回楽しみにしている。

今年使ってよかったものは Windows TerminalVS Code Remote Development の二つ。特にターミナルは Windows で開発する上での一番の pain point を解消してくれた。マイクロソフトが開発ツールに本気を出すとやっぱりすごいなあという印象を新たにしたし、Mac が微妙すぎるのもあって最近はもっぱら Windows ばかり使っている。


入力と出力、あるいは抽象と具現の車輪を回す。どちらを欠いても物事の理解は進まない。今年は具現に時間を割いていろんなことを達成した感はあるが、出がらしの茶葉になった気分もある。行動指針の粒度を細かくして年単位じゃなく四半期ぐらいのスパンで両輪を回すのもいいかもしれない。

Running Wasm on V8 Without JS API

5 Aug 2019

V8 を wasm の実行環境として C++ アプリケーションに組み込んでみよう。

今回使う V8 のバージョンは 7.8.58。samples/hello-world.cc に wasm を実行するがあるけれど、この例は JS 上に定義された API を叩くスクリプトを実行する、という体で書かれている。V8 としては JS から使う前提があるからこういう例になっているのだろうけど、JS を使わずに wasm を実行できないだろうか。

検索すると stackoverflow に同じ思いの先人が質問していて、回答を元にコードを例示してくれていた。ありがたい。ただいくつか API の変更があったようで、v7.8.58 では動かない。V8 の変更履歴を参照しつつ改変したら動くようになった。これで JS スクリプトを経由せずに wasm を実行できるようになった。が、依然として JS API 自体には依存しており処理が減っている感じがしない。

さらに言えば前処理と後処理が冗長なのはいいとしても、値の引き回しに v8::Local<T> を使わないといけないのも釈然としない。今回は JS を使わないのだから、C++ の数値を V8 JS value に変換し、さらにそれから JS value を wasm の数値型に変換する、といったステップを踏むのは無駄に感じる。C++ ⇔ wasm の型変換があればいい。

そう思いつつ V8 のコードを読んでいると c-api.cc というファイルが目についた。冒頭のコメントによると、仕様への提案をベースに書かれたものらしい。wasm を C/C++ に組み込むための提案、だそう。これが求めていたものっぽい。

使い方を調べてみよう。V8 は Wasm C API を third_party/wasm-api 配下に組み込んでいる。V8 を組み込む C++アプリケーションはthird_party/wasm-api/wasm.hhをインクルードすることで wasm の関数の引数や戻り値を v8::Local<T> を使うことなくやりとりできるようになる。提案のを見れば使い方はつかめると思う。要点を抜粋すると以下のような感じ。

  wasm::vec<byte_t> binary; // wasm バイナリ、外部からの入力
  // 実行環境の初期化
  auto engine = wasm::Engine::make();
  auto store_ = wasm::Store::make(engine.get());
  auto store = store_.get();
  // コンパイルとインスタンス生成
  auto module = wasm::Module::make(store, binary);
  auto instance = wasm::Instance::make(store, module.get(), nullptr);
  // 関数の取り出し
  auto exports = instance->exports();
  assert(exports.size() == 1 && exports[0]->kind == wasm::EXTERN_FUNC && exports[0]->func());
  auto run_func = exports[0]->func();
  // 実行
  wasm::Val args[] = { wasm::Val::i32(14), wasm::Val::i32(9) };
  wasm::Val results[1];
  run_func->call(args, results);
  printf("%d\n", results[0].i32());

(コード全体はここを参照)

V8 API を使った例に比べるとだいぶんすっきりとする。ただ内部ではv8::Contextやらv8::Isolateなんかを使っているので初期化等にかかる時間はたぶん大差がない。

一方、関数呼び出しの引数と戻り値に関しては V8 JS value への変換がなくなっており、この部分は高速化が見込めるんじゃないだろうか。

今回は V8 を使うのを前提としたけれど、実際のところ wasm を実行したいだけであれば Wasmer を使う、あるいは WASI に則った実行環境を使うほうが良い。JS のオーバヘッドが気になるなら JS をサポートしてない環境を使うのが理にかなっている。

wasm: Copy と Anyref Export の速度比較

29 Jul 2019

AssemblyScript でライブラリコードの高速化をしてみる の Appendix で anyref を使うとコピーのオーバーヘッドを減らせるかもしれないとの記述があった。おおなるほどそうかも、と思ったが少し考えてみると別のオーバーヘッドが生じそうだ。メモリアクセスをエクスポートされた関数呼び出しに変える必要がある。エクスポートされた関数の呼び出しはインライン化できないし、JS <-> wasm の型変換も必要になる。

ということでどちらが早いのかを 2019 年 7 月時点での評価をしてみようと思う。現時点で anyref が使えるのは Firefox nightly と Chrome。Chrome では --js-flags=--experimental-wasm-anyref フラグを渡す必要がある。

やりたいことと比較する対象を整理する。なんらかのデータを JS 側で Uint8Array として保持している。このデータを wasm で効率的に処理したい。比較する手段は次の二つ。

  1. Copy: wasm モジュールのメモリ領域にデータをコピーする。
  2. Export: データにアクセスする関数を anyref を使ってエクスポートする。

1 は従来のやり方。 JS 側で持っているデータを wasm の メモリ領域にコピーして、コピー先のアドレスとデータの長さを wasm で定義した関数に渡す。2 は冒頭のスライドに書いてあるやり方。anyref があればデータへアクセスする関数を軽量に wasm モジュールに提供できるのでは、というアイデアだ。JS にある Uint8Array buf に対して、 buf[offset] と同等の関数 readByteFromUint8Array(arr: anyref, offset: i32): i32 を wasm モジュールにエクスポートする。

それぞれのオーバーヘッドをみるために Uint8Array の各バイトを足し合わせる単純な関数を wasm で定義する。C で書くとこんなふうなやつ。

/* Copy を使った関数 */
int32_t accumulateCopy(uint8_t *arr, int32_t length) {
    int32_t sum = 0;
    for (int32_t i = 0; i < length; i++) {
        sum += arr[i];
    }
    return sum;
}

/* Anyref を使った関数 */
extern struct Uint8Array;
extern uint8_t readByteFromUint8Array(Uint8Array *arr, int32_t length);
int32_t accumulateAnyref(Uint8Array *arr, int32_t length) {
    int32_t sum = 0;
    for (int32_t i = 0; i < length; i++) {
        sum += readByteFromUint8Array(arr, i);
    }
    return sum;
}

現時点では anyref を出力する安定したコンパイラがないので今回は上記の関数を wat で手書きした。

この 2 つの関数の実行にかかる時間をデータのサイズ毎に計測する。データサイズを 1K, 1M, 10M とした時に手元の Chrome でかかった時間(ミリ秒)のグラフを以下に示す。横軸は対数。

複数回試してみたところ、手元の環境ではデータサイズが 1K の場合は大差ないが、大きいデータサイズではコピーするほうが常に 10 倍ぐらい速い、という結果になった。Firefox でも傾向は同じ。多分どの環境でも似たような結果がでるんじゃないかと思う。使ったベンチマークはここに置いた。

結果を簡単に考察すると: anyref を使ってエクスポートした関数を用意するとコピーにかかる時間は省略できるが、別のオーバーヘッド、すなわち wasm と JS を行き来するコストが支配的かつコピーよりも遅い。TypedArray とそのメソッドを JS を経由せずに wasm から直接呼び出せて、かつその呼び出しをインライン化できないと anyref を使っても速度の向上は見込めなさそう。

Tweaking site design

2 Jul 2019

Nord のカラーパレットが気に入ったので使わせてもらおう。pygments のスタイルも合うように変更する。

柔らかい感じの見た目にしたいので border-radius で枠を少し丸くするように変更。あと line-height も調整した。少し行間がある方が読みやすく感じる。

Rust: Raspberry Pi (Raspbian) 向けの実行バイナリを手軽に作る

23 Jun 2019

Rust で書いたアプリケーションを手元の Raspberry Pi 3 で動かしたい。ベアメタルではなくて OS は Raspbian。

まずは raspi 上でコンパイルしてみる。遅い。自分のアプリをコンパイルするのに 60 分 (!) もかかる。アプリの開発自体は PC でやっているからそんなに頻繁に raspi 用のバイナリを作る必要はないのだけど、さすがにこれではやっていられない。

raspi 上でバイナリを作るのは諦めてクロスコンパイル環境を用意する。Rust は簡単にいろんなアーキテクチャ向けの toolchain 入れられるものの、コンパイルだけでなく Raspbian 向けの実行バイナリをリンクするには GCC のクロスコンパイラ (gcc-arm-linux-gnueabihf) が別途必要になる。開発しているマシンの OS に対する依存が少なく、かつ手軽にリンクまでできる環境を作りたい。となると Docker コンテナでクロスコンパイル環境を作れば良さそうだ。

まずは toolchain のセットアップから。DockerHub に Rust の公式イメージがあるのでこれをベースにする。加えて GCC のクロスコンパイラと Rust のarmv7-unknown-linux-gnueabihf向け toolchain をインストールする。コンテナが起動したら cargo build するようにしておく。Dockerfile はこんな感じ。

FROM rust:1.35

# コンテナ実行時に -v オプションでマウントされるのを前提としている。
WORKDIR /usr/src/myapp

RUN apt-get update && apt-get install -y gcc-arm-linux-gnueabihf
RUN rustup target add armv7-unknown-linux-gnueabihf

CMD ["cargo", "build", "--target", "armv7-unknown-linux-gnueabihf", "--release"]

続いて Rust で書いたアプリのリポジトリ配下の.cargo/configにクロスコンパイル用の設定を書いておく。

[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

あとは Docker イメージ作って実行すれば raspi 上で動くバイナリを生成できる。

$ docker build -t myapp-cross-raspi
$ docker run -it -v "$PWD":/usr/src/myapp myapp-cross-raspi
$ file target/armv7-unknown-linux-gnueabihf/release/myapp
target/armv7-unknown-linux-gnueabihf/release/myapp: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV),
...

ただ、上記の設定だけではcargoのキャッシュがコンテナ内に保持されてしまう。つまりコンテナを作り直すたびに myapp が依存している crates をダウンロードするはめになる。これは資源の無駄なので自分は CARGO_HOME 環境変数を上書きしてホストの適当な場所を指すようにしている。

$ docker run -it -v "$PWD":/usr/src/myapp -e CARGO_HOME="$PWD"/.docker-cargo myapp-cross-raspi

補記

Docker Desktop (Windows と Mac) では最近になって Arm アーキテクチャのコンテナを x86 環境でも動かせるようになったらしい。

Building Multi-Arch Images for Arm and x86 with Docker Desktop - Docker Engineering Blog

つまり Windows と Mac ではコマンド一つ叩くだけで Rasbian 上で動くバイナリをクロスコンパイルできる。

$ docker run -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp arm32v7/rust:1.35 cargo build

こっちのほうが楽だ、と一瞬思ったが、これが結構遅い。内部で QEMU 使っているようだから遅いのはしょうがない気がするが、actix-web を使った単純な hello-world でも手元のマシンで 10 分ぐらいコンパイルに時間がかかった。

参考

梅雨入り前の一日

5 Jun 2019

風鈴がちりん、となった。我が家には風鈴があるのであった。

たしか川崎大社で買い求めたものだ。存在を忘れていたが風情があって良い。

午後にかけてよく晴れた日。ふらっと散歩をして気を良くした。