LoginSignup
5
5

More than 5 years have passed since last update.

最近のemscriptenで変換したcliのIO部分の最適化

Last updated at Posted at 2013-12-01

emscriptenでコマンドラインツール(ukyo/zlib-asm)を変換したやつのIO部分をけっこう最適化という話。

想定の読者

  • emscriptenでコマンドラインツールを変換してる
  • emscriptenのFileSystem APIが使える
  • TypedArrayが使える

普通に変換すると遅い!

  • Module.stdin,Module.stdoutを使うとファイルIO1バイトごとに関数呼び出されるので遅い!
  • デフォルトのFileSystemであるMEMFSはなぜかArray.prototype.sliceを使いまくるので遅い!

なのでTypedArrayだけを使うカスタムFileSystemを作る

Filesystem API · kripken/emscripten WikiとかソースをみながらFileSystemを作る。emscriptenではノードという形でファイルやディレクトリを扱っていて、ノードのプロパティ(node_ops,stream_ops,contents)をうまい具合に設定してやる必要がある。以下、zlibに入ってるサンプルのzpipeを変換した例。

var zlib = (function () {
  var Module = {
    noInitialRun: true
  };

  /* ここにemscriptenが吐き出したおぞましいものが入る */

  // 必要なのだけ実装
  var MYFS = {
    // FS.mount実行時に呼ばれる  
    mount: function (parent, name, mode, rdev) {
      // 3番目の引数はディレクトリフラグとパーミッション
      return MYFS.createNode(null, '/', 16384 | 0777, 0);
    },
    // 作成したノードを操作するメソッド
    node_ops: {
      // ノードの属性を書き込み(例: node.timestamp)
      setattr: function (node, attr) {
        if (attr.mode !== undefined) node.mode = attr.mode;
        if (attr.timestamp !== undefined) node.timestamp = attr.timestamp;
        if (attr.size !== undefined) {
          var contents = node.contents;
          if (contents.length > attr.size) {
            contents = contents.subarray(0, attr.size);
          } else {
            contents = MYFS.expandBuffer(contents, attr.size);
          }
          node.contents = contents;
          node.size = attr.size;
        }
      },
      // ノードを検索するんだけど、不必要なのでMEMFSから拝借
      lookup: MEMFS.node_ops.lookup,
      // 新しいノードを作る
      mknod: function (parent, name, mode, dev) {
        return MYFS.createNode(parent, name, mode, dev);
      }
    },
    // IO系の操作をするメソッド
    stream_ops: {
      // 読み
      read: function (stream, buffer, offset, length, position) {
        var node = stream.node,
            contents = node.contents,
            size = Math.min(contents.length - position, length);

        if (size > 8 && contents.subarray) { // non-trivial, and typed array
          buffer.set(contents.subarray(position, position + size), offset);
        } else {
          for (var i = 0; i < size; i++) {
            buffer[offset + i] = contents[position + i];
          }
        }
        return size;
      },
      // 書き
      write: function (stream, buffer, offset, length, position, canOwn) {
        var node = stream.node,
            contents = node.contents,
            bufferSize = contents.length,
            size = position + length;
        // expand buffer
        if (bufferSize === 0) {
          contents = new Uint8Array(size);
          bufferSize = contents.length;
        }
        contents = MYFS.expandBuffer(contents, size);
        // write
        contents.set(buffer.subarray(offset, offset + length), position);
        node.contents = contents;
        node.size = size;

        return length;
      }
    },
    // ノードの作成。node_ops, stream_ops, contentsを設定する必要がある
    createNode: function (parent, name, mode, dev) {
      var node = FS.createNode(parent, name, mode, dev);
      node.node_ops = MYFS.node_ops;
      node.stream_ops = MYFS.stream_ops;
      node.contents = [];
      node.timestamp = Date.now();
      if (parent) parent.contents[name] = node;
      return node;
    },
    // FS.createDataFileっぽく使えるやつ
    createFile: function (parent, name, data, r, w) {
      var node = FS.createFile(
        parent,
        name,
        {},
        r,
        w
      );
      node.contents = data;
      node.node_ops = MYFS.node_ops;
      node.stream_ops = MYFS.stream_ops;
      return node;
    },
    // バッファサイズの拡張。とりあえず倍々
    expandBuffer: function (buffer, size) {
      if (buffer.length >= size) return buffer;
      var bufferSize = buffer.length, _buffer;
      while (bufferSize < size) bufferSize *= 2;
      var _buffer = new Uint8Array(bufferSize);
      _buffer.set(buffer);
      return _buffer;
    }
  };

  // MYFSをマウント。絶対必要!  
  FS.mount(MYFS, {}, '/');

  function $run (args, input) {
    // ファイルが残っている場合は削除
    try {
      FS.destroyNode(FS.lookupPath('/input').node);
    } catch (e) {} 
    try {
      FS.destroyNode(FS.lookupPath('/output').node);
    } catch (e) {}
    // ファイルを作成
    MYFS.createFile(
      '/',
      'input',
      input,
      true,
      true
    );
    var outputNode = MYFS.createFile(
      '/',
      'output',
      new Uint8Array(0),
      true,
      true
    );
    // main関数呼び出し
    Module.callMain(args);
    // おしりを削って返す
    return outputNode.contents.subarray(0, outputNode.size);
  }

  return {
    run: $run,
    compress: $run.bind(null, []),
    decompress: $run.bind(null, ['-d'])
  };

})();

ここまでやってasm.js用に変換するとようやく大体ネイティブの2〜3倍くらいのスピードで動いてくれます。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5