Barbarism begins at internet:2021年12月16日分

2021/12/16(Thu)

[プログラミング] sljit覚書(その1)

ちょこっと弄っただけなのでさっぱりわからんのだが、来世で使う可能性もあるので生まれ変わった僕が検索でヒットするようメモでも残しておこう。 といいつつ今の時代は検索エンジンからこのチラシの裏に辿り着くことなんてまず不可能だろうけどな!

@sljit_create_compiler

SLJIT_API_FUNC_ATTRIBUTE struct sljit_compiler* sljit_create_compiler(void *allocator_data, void *exec_allocator_data);

その名の通りJust-In-Timeコンパイラのインスタンスを生成する、失敗したらNULLが返る。

struct sljit_compiler *c;
c = sljit_create_compiler(NULL, NULL);
if (c == NULL)
	abort();

引数2つあるけど通常利用であればどちらもNULLでいい、この引数はメモリ管理をオーバーライドするような特殊な使い方をする時にのみ必要なものだから。

まず第一引数allocator_dataについて、sljitは通常メモリアロケーションに標準のmalloc/freeを使うが、freestanding環境などでこれらの関数が使えない場合は

  • -DSLJIT_STD_MACROS_DEFINED=0を指定してC標準のmalloc/free/memcpy/NULLを無効にする
  • SLJIT_MALLOC/SLJIT_FREE/SLJIT_MEMCPY/NULLをオレオレ実装で再定義する
  • 渡したallocator_dataはsljit_compiler構造体が保持する
    struct sljit_compiler {
    ...
            void *allocator_data;
    ...
    };
    
  • SLJIT_MALLOC/SLJIT_FREEの第二引数にこのallocator_dataが渡される
    #ifndef SLJIT_MALLOC
    #define SLJIT_MALLOC(size, allocator_data) malloc(size)
    #endif
    
    #ifndef SLJIT_FREE
    #define SLJIT_FREE(ptr, allocator_data) free(ptr)
    #endif
    
  • よってメモリチャンクなり関数ポインタなりフラグ情報なりなんなり、オレオレmalloc/free実装にあわせてお好きにどうぞ

という手順でオーバーライドする、まぁbpf(4)のようにkernelにsljitを組み込むなどの用途でもない限り必要は無い。

お次にexec_allocator_data、こちらはJITコンパイルした実行可能コード用のアロケーター、同様の手順でオーバーライド可能。

  • -DSLJIT_EXECUTABLE_ALLOCATOR=0を指定する
  • SLJIT_MALLOC_EXEC/SLJIT_FREE_EXEC/SLJIT_EXEC_OFFSETをオレオレ実装で再定義する
  • exec_allocator_dataについてはallocator_dataと同じように使う
  • 以下同文

なお何故SLJIT_MALLOC/SLJIT_FREEのペアとSLJIT_MALLOC_EXEC/SLJIT_FREE_EXECのペアのふたつも必要なのかというと、これはいまどきのCPUやOSはセキュリティ上の理由で データ実行防止というというセキュリティ機構でスタックやヒープ上でのコード実行を禁止しているから。 よってPROT_EXECフラグつきでmmap(2)呼んでページを実行可能にマークするとか、W^XやPaXそしてExcec SheildなどでNXビットを有効にするなどの処理が必要になる。 まぁ知らんでいいこと知りたい暇なオッサンは隅っこでsljitExecAllocator.c/sljitProtExecAllocator.c/sljitWXExecAllocator.cのコードでも読んでろ。

よしここまで危険性を書いとけば意味も無くオーバーライドしてハードラックとダンスっちまうやつも現れないだろう。 そもそも本当にJITを使う必要性があるのかからよーく考えような、Microsoft Edgeブラウザなんかは性能向上より安全性をとってJIT無効にするモードを用意するくらいやぞ。

@sljit_compiler_verbose

SLJIT_API_FUNC_ATTRIBUTE void sljit_compiler_verbose(struct sljit_compiler *compiler, FILE* verbose);

冗長モードをセットする、開発中は標準エラー出力にでもデバッグ情報を出すようにしておく。

sljit_compiler_verbose(c, stderr);

これ-DSLJIT_VERBOSE=0するとコンパイルエラーになるのがちょっと不便、空文におきかえてくれればいいのにね-DNDEBUG=1の時のassert(3)みたいに。

余談だけどデフォルトでSLJIT_DEBUGが有効なので、アサーション無効にしたければ-DSLJIT_DEBUG=0でコンパイルする必要があることにも注意。

@sljit_emit_enter

SLJIT_API_FUNC_ATTRIBUTE sljit_s32 sljit_emit_enter(struct sljit_compiler *compiler,
        sljit_s32 options, sljit_s32 arg_types, sljit_s32 scratches, sljit_s32 saveds,
        sljit_s32 fscratches, sljit_s32 fsaveds, sljit_s32 local_size);

これはx86のenter命令的なもの?といえばいいのかな、関数(=JITコンパイルされた実行コード)の入口。

まず第1引数のoption、これは今のところ

  • SLJIT_F64_ALIGNMENT … アライメントをsjjit_sw型(=long)でなくsljit_f64型(=double)のサイズに合わせる

しか無いので、32bit環境でも無い限り0でいい。

第2引数のarg_typesはJITコンパイルされた実行コードを関数ポインタとして呼び出すときの

  • 引数の個数 … 最大3個まで、SLJIT_ARG1~3マクロのORで表現する
  • 引数の型 … sljit_sw(符号ありワード型)かsljit_uw(符合なしワード型)のいずれか、SLJIT_ARG1~3マクロの引数にSWあるいはUWをセットすることで表現する

を指定するもの、通常引数ってスタックに置かれるもんだけどStack-Less JITの名の通りスタックが無いのでこの制限となる、詳しい話は回を改めて説明する。

以下は一例、実行コードを

typedef sljit_sw (*func_t)(sljit_sw, sljit_sw, sljit_sw);

と3つの引数を渡して呼び出すのであれば、arg_typesの指定は

sljit_emit_enter(c, SLJIT_ARG1(SW)|SLJIT_ARG2(SW)||SLJIT_ARG3(SW), ...);

となる、これらの引数はJITコード内だとSLJIT_S0~2という汎用レジスタに置かれる、

そんでsljitのアセンブラ(LIR)レジスタには

  • 汎用レジスタ(ワードレジスタ) … 最大は SLJIT_NUMBER_OF_REGISTERS 個
  • 浮動小数点用レジスタ … 最大は SLJIT_NUMBER_OF_FLOAT_REGISTERS 個

の2種類があり、更にそれぞれに

  • 「Scratched」 … 一時的な利用のためのレジスタ、関数の呼出前後で値が保持されない可能性がある。
  • 「Saved」 … 〃値は常に保持される

の2種類があるので合計で4種類となる。

これらにアクセスするためには

  • 汎用Scratchedレジスタ
    • SLJIT_R0~9、それ以上はSLJIT_R(N)
    • Nの最大はSLJIT_NUMBER_OF_SCRATCH_REGISTERS
  • 汎用Savedレジスタ
    • SLJIT_S0~9、それ以上はSLJIT_S(N)
    • Nの最大はSLJIT_NUMBER_OF_SAVED_REGISTERS
  • 浮動小数点数用Scratchedレジスタ
    • SLJIT_FR0~5、それ以上はSLJIT_FR(N)
    • Nの最大はSLJIT_NUMBER_OF_SCRATCH_FLOAT_REGISTERS
  • 浮動小数点数用Savedレジスタ
    • SLJIT_FS0~5、それ以上はSLJIT_FS(数値)
    • Nの最大はSLJIT_NUMBER_OF_SAVED_FLOAT_REGISTERS

というマクロを使う。

んで第3~6引数にはこの4種類のレジスタを最大何個使用するかを指定するわけ。

  • 実行コードが戻り値を返す場合、scratchesには返却値を格納するレジスタ(SLJIT_RETURN_REG)としてSLJIT_R0が必要なので、1以上の値を指定する。
  • savedsにはさっきのarg_typesで指定した引数の個数Nだけは必要になるので、N以上の値を指定する。
  • fscratchesとfsavedsは浮動小数点数を使わないなら0でいい。

ちゅーかんじ。

なおSLJIT_NUMBER_OF_*はCPUによって値は異なるので移植性に注意すること、現状でサポートされるCPUの最大公約数をとると

  • SLJIT_NUMBER_OF_REGISTERS は12個(ただしx86_32ではうち6は仮想)
  • SLJIT_NUMBER_OF_SCRATCH_REGISTERS は6個

となる、またScratchとSavedはオーバーラップする場合があることに注意。

     R0   |        |   R0 is always a scratch register
     R1   |        |   R1 is always a scratch register
    [R2]  |   S2   |   R2 and S2 represent the same physical register
    [R3]  |   S1   |   R3 and S1 represent the same physical register
    [R4]  |   S0   |   R4 and S0 represent the same physical register

オーバーラップしてるかどうかは

SLJIT_NUMBER_OF_REGISTERS - SLJIT_NUMBER_OF_SCRATCH_REGISTERS < SLJIT_NUMBER_OF_SAVED_REGISTERS

の真偽で判るはず。

第7引数local_sizeはローカルスタックサイズの指定、0~SLJIT_MAX_LOCAL_SIZE(65536)の範囲で指定するけど現状使われてないので0でいいや。 これ将来的にStack-Less JITといいつつスタックエミュレーションも実装する予定なのかな、よく判らん。

@sljit_emit_return

SLJIT_API_FUNC_ATTRIBUTE sljit_s32 sljit_emit_return(struct sljit_compiler *compiler, sljit_s32 op,
        sljit_s32 src, sljit_sw srcw);

これはsljit_emit_enterと対になるx86でいうとこのleave命令みたいな?もの、つまり関数(=JITコンパイルされた実行コード)の出口。

引数については

  • 第2引数のopにSLJIT_MOV*命令がセットされてる場合
    • 第3引数に指定したレジスタの値
    • あるいは第3~4引数で指定したメモリ範囲
    をSLJIT_RETURN_REGにセットして返す
  • SLJIT_UNUSEDがセットされてる場合には戻り値は無し(その場合は第3~4引数は0にする)

ちゅーかんじ、なおSLJIT_MOV*命令とSLJIT_MEMマクロによるメモリ範囲指定はまた回を改めて説明する。

さっきの例、実行コードが

typedef sljit_sw (*func_t)(sljit_sw, sljit_sw, sljit_sw);

とsljit_sw型を返すのであれば

sljit_emit_return(c, SLJIT_MOV, SLJIT_R0, 0);

みたいにするとSLJIT_R0の値が戻り値として返される。

@sljit_generate_code

SLJIT_API_FUNC_ATTRIBUTE void* sljit_generate_code(struct sljit_compiler *compiler);

これはJITコンパイラから実行コードを生成する、戻り値のポインタは関数ポインタにキャストすれば実行可能ちゅーこと。

@sljit_free_compiler

SLJIT_API_FUNC_ATTRIBUTE void sljit_free_compiler(struct sljit_compiler *compiler);

これはJITコンパイラを破棄する、実行コードは破棄されない。

@sljit_free_code

SLJIT_API_FUNC_ATTRIBUTE void sljit_free_code(void* code, void *exec_allocator_data);

これは実行コードを破棄する、第2引数はさっきのオーバーライドをしてないのでNULLでいい。

ここまでの例をざっとまとめて、引数1~3を加算して返すJITコードの例。

#include <stddef.h>
#include <stdio.h>

#include <sljitLir.h>

typedef sljit_sw (*func_t)(sljit_sw, sljit_sw, sljit_sw);

int
main(void)
{
	struct sljit_compiler *c;
	func_t func;
	sljit_sw ret;

	c = sljit_create_compiler(NULL, NULL);
	if (c == NULL)
		abort();
	sljit_compiler_verbose(c, stderr);

	if (sljit_emit_enter(c, 0, SLJIT_ARG1(SW)|SLJIT_ARG2(SW)|SLJIT_ARG3(SW), 1, 3, 0, 0, 0) != SLJIT_SUCCESS)
		abort();
	/* R0 = S0 */
	if (sljit_emit_op1(c, SLJIT_MOV, SLJIT_R0, 0, SLJIT_S0, 0) != SLJIT_SUCCESS)
		abort();
	/* R0 += S1 */
	if (sljit_emit_op2(c, SLJIT_ADD, SLJIT_R0, 0, SLJIT_R0, 0, SLJIT_S1, 0) != SLJIT_SUCCESS)
		abort();
	/* R0 += S2 */
	if (sljit_emit_op2(c, SLJIT_ADD, SLJIT_R0, 0, SLJIT_R0, 0, SLJIT_S2, 0) != SLJIT_SUCCESS)
		abort();
	/* return R0 */
	if (sljit_emit_return(c, SLJIT_MOV, SLJIT_R0, 0) != SLJIT_SUCCESS)
		abort();

	func = (func_t)sljit_generate_code(c);
	if (func == NULL)
		abort();
	sljit_free_compiler(c);

	ret = (*func)(1, 2, 3);

	printf("%ld\n", ret);

	sljit_free_code((void *)func, NULL);

	exit(0);
}

これをコンパイルし実行すると

$ gcc -I./sljit/sljit_src unko.c ./sljit/sljit_src/sljitLir.c -o unko
$ ./unko
  enter options: args[sw,sw,sw] scratches:1 saveds:3 fscratches:0 fsaveds:0 local_size:0
  mov r0, s0
  add r0, r0, s1
  add r0, r0, s2
  return r0
6

はい、1 + 2 + 3 = 6が表示されました。

@次回

いつになるかは未定だけどLIRの命令についてざっと説明する予定。