PHPWebのTemplateEngineをWebAssemblyに置き換えてみる

こんにちはメルカリアドベントカレンダー 8日目はWebPlatform所属のmkazutaka(twitter: _mkazutaka)がお送りします。

みなさんWebAssembly(Wasm)についてどれくらい知っておられるでしょうか。「名前は聞いたことある」「ブラウザ上で実行可能なバリナリファイルでしょ」という方も多いと思います
最近ではそういった一面に加え、WebAssemblyをウェブの外で使うためのシステムインターフェースを決めるWASIプロジェクトの発表FastlyのOSSであるLucetといったブラウザを超えた活用が増えてきております

ではメルカリのようなWebサイトでどのようにWasmを使えるのでしょうか。少し考えてみてください

そう意外に思いつかないのです。fitzgen/dodrio(Rust*WebAssemblyで仮想DOMを実現するライブラリ)もありますがメルカリのWebに導入するのは大げさですし、かといってそこまで計算量の多い処理もなかなかないし(とはいえアドベントカレンダーのネタが)…というわけで

無理やりひねり出したのが

「テンプレートエンジンをWasmで置き換えるのはどうなんだ?」

やってみます

PHP上からWasmを呼び出すための拡張モジュールとしてwasmerio/php-ext-wasmがあります。最初にこれをビルドしPHPからWasmを呼び出せるようにします

||*'-') <  git clone git@github.com:wasmerio/php-ext-wasm.git
||*'-') <  cd php-ext-wasm
||*'-') <  brew install just
||*'-') <  just build

インストールした拡張モジュールを有効にするためphp.iniに以下を追記します。php.iniの場所は環境により異なります。自分の環境だと /usr/local/etc/php/7.3/php.ini でした

extension = wasm

ここで一度実行できるか試してみます

||*'-') <  php examples/simple.php
int(42)

次にWasmファイルを作成します。今回はRustのコードをWasmにコンパイルします。
全体的なフローは、PHPから関数を呼び出す -> Wasmで結果を返す -> PHPで出力するとなります。
PHPとWasm間で文字列の受け渡しにはメモリを経由します.wasmerio/php-ext-wasmexampleがあるためこちらを参考にします。

Wasmから出力結果を返すためにRustで書かれたテンプレートエンジンを使用します。今回は
djc/askamaを使います。Rustの記述方法は説明しませんが公式ドキュメントを参考にみてください

今回はbase.htmlを継承するbench.htmlを作成し、それらをコンパイル時に組み込みます

<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{% block title %}{{ title }} - My Site{% endblock %}</title>
    {% block head %}{% endblock %}
</head>
<body>
<div id="content">
{% block content %}{% endblock %}
</div>
</body>
</html>
<!-- bench.html -->
{% extends "base.html" %}
{% block title %}{{title}}{% endblock %}
{% block head %}
<style>
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p>Hello, world!</p>
{% endblock %}
extern crate askama;
use askama::Template;
use std::ffi::{CStr, CString};
use std::mem;
use std::os::raw::{c_char, c_void};
#[derive(Template)]
#[template(path = "bench.html")]
struct BenchTemplate<'a> {
title: &'a str,
}
#[no_mangle]
pub extern "C" fn bench() -> *mut c_char {
let bench = BenchTemplate{
title: "this is title"
};
unsafe { CString::from_vec_unchecked(bench.render().unwrap().into_bytes()) }.into_raw()
}
#[no_mangle]
pub extern "C" fn allocate(size: usize) -> *mut c_void {
let mut buffer = Vec::with_capacity(size);
let pointer = buffer.as_mut_ptr();
mem::forget(buffer);
pointer as *mut c_void
}
#[no_mangle]
pub extern "C" fn deallocate(pointer: *mut c_void, capacity: usize) {
unsafe {
let _ = Vec::from_raw_parts(pointer, 0, capacity);
}
}

あとはこれらをbuildします

||*'-') <  wasm-pack build

プロジェクト直下 target/wasm32-unknown-unknown/release にWasmファイルができています
あとはPHP側からWasmファイルを呼び出しコールすれば完了です。
それでは呼び出し用のPHPのプロジェクトを作ります。PHPからWasmを便利に呼び出すためにphp-wasmをインストールをします。また今回はベンチマーク比較のためにphpbench/phpbenchとPHPのテンプレートエンジンtwigphp/Twigもインストールします

||*'-') <  mkdir wasm-php
||*'-') <  cd wasm-php
||*'-') <  composer init  # よしなにyes
||*'-') <  composer require php-wasm/php-wasm
||*'-') <  composer require twig/twig:2.12.2
||*'-') <  composer require phpbench/phpbench @dev --dev

コードを書いていきます。最初にTwig用のコードを書きます

<?php
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
class Twig
{
/** @var Environment */
private $twig;
public function __construct()
{
$loader = new FilesystemLoader(__DIR__ . '/../templates');
$this->twig = new Environment($loader);
}
public function renderBench(): string
{
return $this->twig->render('bench.html.twig', [
'title' => 'this is title'
]);
}
}

同じようにWasm側のコードを書きます。wasmerio/php-ext-wasmで用意されているModuleCache機能を使用しています

<?php
class Wasm
{
const KEY = 'wasm';
const WASM_PATH = '../wasm-template-engine/target/wasm32-unknown-unknown/release/wasm_template_engine.wasm';
const CACHE_DIR = __DIR__ . "/module_caching";
/** @var \Wasm\Cache\Filesystem */
private $cache;
private $wasm_instance;
public function __construct()
{
$this->cache = new \Wasm\Cache\Filesystem(static::CACHE_DIR);
if ($this->cache->has(static::KEY)) {
$this->wasm_instance = $this->cache->get(static::KEY)->instantiate();
return $this->wasm_instance;
}
$module = new \Wasm\Module(static::WASM_PATH);
$this->cache->set(static::KEY, $module);
$this->wasm_instance = $module->instantiate();
}
public function renderBench(): string
{
$wasm = $this->wasm_instance;
$input_pointer = $wasm->allocate(1);
$memory_buffer = $wasm->getMemoryBuffer();
$output_pointer = $wasm->bench();
$memory = new Wasm\Uint8Array($memory_buffer, $output_pointer);
$output = '';
$nth = 0;
while (0 !== $memory[$nth]) {
$output .= chr($memory[$nth]);
++$nth;
}
$length_of_output = $nth;
return $output;
}
}

続いてベンチマーク用のファイルを作成します

// bench.php
<?php
require __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/wasm.php';
require_once __DIR__ . '/twig.php';
/**
 * Class RenderingBench
 * @BeforeMethods({"init"})
 * Warmup(0)
 * Revs(0)
 * @Iterations(20)
 */
class RenderBench
{
/** @var Wasm */
private $wasm;
/** @var Twig */
private $twig;
public function init()
{
$this->wasm = new Wasm();
$this->twig = new Twig();
}
public function benchTwig()
{
$this->twig->renderBench();
}
public function benchWasm()
{
$this->wasm->renderBench();
}
}

最後にこれを実行します。なおrender関数自体は1リクエスト内で複数回実行されることがないだろうと想定の上、Warmupを0、Revsを0にしてIterationsのみ20回にしてします

||*'-') <  ./vendor/bin/phpbench run bench.php
PhpBench @git_tag@. Running benchmarks.
\RenderBench
benchTwig...............................I19 [μ Mo]/r: 8,372.950 8,214.466 (μs) [μSD μRSD]/r: 671.125μs 8.02%
benchWasm...............................I19 [μ Mo]/r: 1,535.150 1,459.804 (μs) [μSD μRSD]/r: 179.166μs 11.67%
2 subjects, 40 iterations, 2 revs, 0 rejects, 0 failures, 0 warnings
(best [mean mode] worst) = 1,394.000 [4,954.050 4,837.135] 2,176.000 (μs)
⅀T: 198,162.000μs μSD/r 425.145μs μRSD/r: 9.843%

ベンチマークをみると、Twigでのrender描画が8,372マイクロ秒でWasmのほうが1,535マイクロ秒となっています

お、Twigに比べてWasmのほうが早い!!!これは!

と思いそうですが実は少しズルをしております。PHPからWasmを呼び出す際Wasm用のInstanceを作成しているのですが、コードを見るとわかるように今回はそれをベンチマーク計測外にしているのです

これらをベンチマーク計測内、下記のようなコードに書き換えて再度実行してみると..

    public function benchTwig()
{
$this->twig = new Twig();
$this->twig->renderBench();
}
public function benchWasm()
{
$this->wasm = new Wasm();
$this->wasm->renderBench();
}
||*'-') <  ./vendor/bin/phpbench run bench.php
PhpBench @git_tag@. Running benchmarks.
\RenderBench
benchTwig...............................I19 [μ Mo]/r: 11,458.850 11,311.055 (μs) [μSD μRSD]/r: 411.527μs 3.59%
benchWasm...............................I19 [μ Mo]/r: 12,167.600 12,149.659 (μs) [μSD μRSD]/r: 281.442μs 2.31%
2 subjects, 40 iterations, 2 revs, 0 rejects, 0 failures, 0 warnings
(best [mean mode] worst) = 10,936.000 [11,813.225 11,730.357] 12,399.000 (μs)
⅀T: 472,529.000μs μSD/r 346.484μs μRSD/r: 2.952%

Twigの方がわずかに早いですね!なるほどなるほどという気持ち

いかがでしたでしょうか、WasmはInstance生成にオーバーヘッドはあるものの実行自体は早いというのがわかります

一方で実際やってみると

  • 複雑なテンプレートになるとコンパイル時間/Wasmファイルのサイズが気になる
  • PHPから呼び出すときに引数を渡すの辛そう(jsonにしてserializeしてrust で受け取ってdeserializeするとか..?)
  • 分散モノリス感

な印象を抱きました。とはいえ、意外とやってみると面白いものですね

これを気にWasm等興味を持っていただけると幸いです。自分ももう少しいろんなことができるよう精進していきたいと思います

全体のコードはここにあります。いろいろと遊んでみてください

github.com

明日の執筆担当は、メルカリ AIチームの@araseさんです。それでは引き続きお楽しみください!

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加