libClang/LLVM で C++ コードの JIT コンパイルを行う.

libClang/LLVM を使って C++ コードを JIT コンパイルする環境を整えます.

問題点

構成としては以下のようになります.

+------------------+                           includes
|                  |   JIT +------------+    +--------------+
|     C++ app      | <--+- |  C++ code  |<---| STL header   |
|                  |    |  +------------+    +--------------+
+------------------+    |                    | C header     |
|  libClang        |    |                    +--------------+
+------------------+    |                          |
|  libLLVM(MCJIT)  |    |                          |
+------------------+    |                          |
|  STL runtime     |----+--------------------------+
+------------------+

(既存のを含め) C++ コードをそのまま JIT コンパイルしたいとなると, 通常, C++ コードは STL を使用しているため, ランタイム周りやヘッダッファイルの在りかの問題がからんできまして, この辺りで不都合が起こります.
app 側(libClang,libLLVM, STL runtime)と, JIT 実行される C++ コードは同じ ABI, STL であり, また RTTI/例外の構成も同じである必要があります.

C 言語ですと STL が無い + ABI 問題が無いため, JIT 環境は整えやすいですが, 漢たるもの, C++ の JIT やりたいですね.

LLVM のサイトには prebuilt バイナリがありますが http://releases.llvm.org/download.html,

これは基本的には clang/llvm ツール群のスタンドアロン実行を想定していて, 付属の libLLVM* などのライブラリは, ABI 違いや stdlib 違い(プレビルトは Ubuntu との互換性を考慮してか gnustl/libgcc でビルドされている)などで, 自作の JIT アプリでこれをそのまま使うとリンクエラーなど起きてしまいます.

Ubuntu 16.04 で試したところ, prebuilt の 6.0.0 はシンボル解決エラー. 5.0.1 はリンクできるものの, JIT をすると thread_struct あたりが見つからないなどのエラーが出ました.

手順

libClang/LLVM をソースからコンパイルして環境を整えます.
ホストコンパイラには prebuilt の clang++ を使います(これにより, ユーザがビルドした clang や, gcc でコンパイルしたときに発生しそうな問題を減らします)

libcxx のビルド(単体向け)

後述する clang/llvm を含め一式ビルドしたほうが, のちのち面倒が無いと思いますので, ここはスキップして一式ビルドをするのをおすすめします.

参考までに単体でビルドする手順を残しておきます.

LLVM_DIR=$HOME/local/clang+llvm-6.0.0-x86_64-linux-gnu-ubuntu-14.04
LLVM_SRC_DIR=../llvm
LIBCXXABI_DIR=../libcxxabi
LIBCXX_DIR=../libcxx
INSTALL_DIR=$HOME/local/llvm-libcxx-dist
CLANG_BIN=${LLVM_DIR}/bin/clang
CLANGXX_BIN=${LLVM_DIR}/bin/clang++

curdir=`pwd`

# build libcxxabi
$ rm -rf ../libcxxabi-build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} -DLIBCXXABI_LIBCXX_PATH=${LLVM_DIR} -DCMAKE_C_COMPILER=${CLANG_BIN} -DCMAKE_CXX_COMPILER=${CLANGXX_BIN} -B../libcxxabi-build -H${LIBCXXABI_DIR}

$ cd ../libcxxabi-build
$ make && make install
$ cd ${curdir}

# build libcxx with libcxxabi
$ rm -rf ../libcxx-build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} -DLLVM_PATH=${LLVM_DIR} -DCMAKE_C_COMPILER=${CLANG_BIN} -DCMAKE_CXX_COMPILER=${CLANGXX_BIN} -DLIBCXX_CXX_ABI=libcxxabi -DLIBCXX_CXX_ABI_INCLUDE_PATHS=${LIBCXXABI_DIR}/include -B../libcxx-build -H${LIBCXX_DIR}

$ cd ../libcxx-build
$ make && make install
$ cd ${curdir}

よりピュアな環境を構築するのであれば,

https://stackoverflow.com/questions/25840088/how-to-build-libcxx-and-libcxxabi-by-clang-on-centos-7/25840107

にあるように複数回に分けて libcxx をビルドするとよいでしょう.

LLVM のビルド

clang, llvm を libcxx を使うようにしてビルドします.
以下のサイトを参考に, libcxx, libcxxabi も含め一式 clang/llvm をビルドします.

http://chiselapp.com/user/ttmrichter/repository/gng/doc/trunk/output/blog/2016/01-llvm-clang-missing-manual.html

を参考に一括でビルドするスクリプトです.
ホストコンパイラには prebuilt clang/LLVM を使うとよいです.

-DLLVM_ENABLE_CXX=On にして LLVM ビルドします.

また, TERMINFO を Off にして libtinfo 依存を無くしておくとよいです.

build type は Release or MinSizeRel にしておきましょう.
Debug だと clang の動作が遅くなり使い物になりません! RelWithDebInfo であれば clang の動作は速くはなりますが, ファイルサイズは 30 GB くらい消費してしまいます!

デフォルトでは RTTI/例外なしでビルドされます. 必要であれば RTTI/例外を有効にしてビルドしてください.

version=6.0.0
bases="llvm-${version}.src cfe-${version}.src compiler-rt-${version}.src"
bases="${bases} libcxx-${version}.src libcxxabi-${version}.src"
bases="${bases} libunwind-${version}.src lld-${version}.src lldb-${version}.src"              \
#bases="${bases} openmp-${version}.src polly-${version}.src"
#bases="${bases} clang-tools-extra-${version}.src"

llvm_root=llvm-${version}.src

rm -rf ${llvm_root}

# "test-suite-${version}.src" is not included because reasons.  It's easy enough
# to add if you want it.

for base in ${bases}
do
  if [ ! -f "${base}.tar.xz"]; then wget -t inf -c http://llvm.org/releases/${version}/${base}.tar.xz; fi
  tar xvf ${base}.tar.xz
done


mv -v cfe-${version}.src ${llvm_root}/tools/clang
#mv -v clang-tools-extra-${version}.src ${llvm_root}/tools/clang/tools/extra
mv -v compiler-rt-${version}.src ${llvm_root}/projects/compiler-rt
mv -v libcxx-${version}.src ${llvm_root}/projects/libcxx
mv -v libcxxabi-${version}.src ${llvm_root}/projects/libcxxabi
mv -v libunwind-${version}.src ${llvm_root}/projects/libunwind
mv -v lld-${version}.src ${llvm_root}/tools/lld
mv -v lldb-${version}.src ${llvm_root}/tools/lldb
#mv -v openmp-${version}.src ${llvm_root}/projects/openmp
#mv -v polly-${version}.src ${llvm_root}/tools/polly

mkdir ${llvm_root}/build
cd ${llvm_root}/build
cmake -DCMAKE_CXX_COMPILER=/path/to/clang++ -DCMAKE_C_COMPILER=/path/to/clang -DLLVM_ENABLE_TERMINFO=Off -DLLVM_ENABLE_LIBCXX=On -DCMAKE_INSTALL_PREFIX=$HOME/local/clang+llvm-${version} -DCMAKE_BUILD_TYPE=MinSizeRel ..
make -j8 && make install

アプリ側での JIT コンパイル設定

上記でビルドした clang++/libclang/libLLVM で JIT アプリをコンパイルします.

注意点

アプリ側の関数を JIT される C++ コードから呼ぶ場合は, アプリを shared library(+ PIC, PIE)でコンパイルします. そうしないと, JIT C++ コード側からアプリにリンクされているライブラリのシンボルが見えなくなります(正確には, 同じヘッダ定義を読んでいるのであればシンボルは解決できるが, コードの配置位置がずれるなどで関数(メソッド)を呼んだときに segmentation faulut する).

設定例

LLVM の JIT サンプルと, softcompute の JIT 周りを参考ください.

https://github.com/lighttransport/softcompute/blob/master/src/jit-engine.cc

JIT アプリ側では, libClang でコンパイルするときのパラメータを調整します.
-nostdinc++ で標準のヘッダファイルを探索せず, ユーザ定義のヘッダを見るようにします.
-I で libcxx のヘッダのパスを, -isystem でビルドした clang/LLVM のシステムインクルードパスを設定します.
(上の jit-engine.cc ではまだ未反映)

-nostdinc++ -I/path/to/llvm-libcxx-dist/include/c++/v1 -isystem=/path/to/llvm-libcxx-dist/lib/clang/7.0.0/include

(7.0.0 は適当に置き換えてください)

-isystem を使わないと, include_next で stddef.h が見つからないなどのエラーがでます.
_GNU_SOURCE も定義しないほうがいいかもしれません.

上記のパス設定は, アプリをビルドするときと, JIT コンパイルするときの設定の両方で行います.

name mangling

アプリから C++ JIT コードの C++ 関数を直接呼びたい場合は, Module::getFunction には mangle 後のシンボル名を指定します.

これにより, STL を含んだ C++ コードを JIT コンパイルできます!

まとめ

libClang/LLVM を使って C++ コードの JIT をできるようにしました.

問題点

そこそこ簡単なコードでも, テンプレート多用している C++ コードを JIT コンパイルはやっぱり遅くてつらい(3~4 秒くらいかかる. テンプレートなしだと 1~2 秒くらい).

Windows の場合は, Visual Studio(MSC) でコンパイルする場合は msvcrt ランタイムと, Visual Studio の C++ header が必要になりますが, これが LLVM JIT 側が対応しているかは不明です.
最近は clang for Visual Studio も出てきて, windows 関連のヘッダは clang でも問題なくパースできるようになってきているようですが, MSVC との ABI 絡みはどうでしょうか?

また, 動作したとしても, MSVCRT と MSC STL ヘッダーファイル再配布の問題があります.

Windows の場合は, 再配布の問題などが無い MinGW ベースとするのがよさそうです.

TODO

  • NanoSTL https://github.com/lighttransport/nanostl を利用した C++ JIT を試す.
  • clang module あたりでプリコンパイルヘッダを試す
  • テンプレートメタプログラムのコンパイルを早くする方法を探す
  • 例外を有効にして, JIT した C++ プログラムが実行時エラーになったときでも app 本体が落ちないようにする.
  • OrcJIT などの新しい JIT 形式を試す(筆者は MCJIT が出た頃の知識で止まっているので OrcJIT をよく知らない)
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.