LLVMを始めよう! 〜 LLVM IRの基礎はclangが教えてくれた・Brainf**kコンパイラを作ってみよう 〜

コンパイラを作ってみたいと思っていても、アセンブリ言語はよくわからない。 パーサーみたいなコードは書いたことがあるけれど、コード生成の処理はさっぱりだ。 実行ファイルをバイナリエディターで見るとかなにそれ怖い。

そんな私なのですが、LLVMに興味を持ち始めています。 SwiftやRust、あるいはEmscriptenなど、近年注目されている言語やコンパイラ技術の中枢にはLLVMがあります。 アセンブリはよく分からなくてもLLVMを使いこなせるようになれば、マルチプラットフォームで実行ファイルを生成できる言語処理系を作るのではないか。 コンパイラ作ってみたいな、LLVMを使ってみようかなと思っている今日このごろです。

ところが、いざLLVMを勉強しようと思ってもどこから始めればいいかよく分かりませんでした。 マニュアルは巨大で読む気が起きないし、リファレンスを見てもさっぱりです。 雰囲気はわかるんです、インストラクションっていうのはプリミティブな命令のことでしょう、そのリファレンスは便利なんでしょう。 でも素人にはどこから手を付けてよいのかわからない。 LLVMを用いた処理系の入門として名高いKaleidoscopeをやってみようかなと思って始めても、Lexerを作ってParserを作って、「ああ、C++なんて久々に書いたなぁ」なんて言っているうちにLLVM IRにたどり着かずに飽きてしまう。 C++が肌に合わないんだと思ってGo言語のLLVM bindingを使ってコードを書こうとしても、Go bindingのソースコードLLVM本体のコードをあっちこっち飛んで回って疲弊してしまう。 他の人が作った処理系のコードを読んでみようと思ってcloneしてみても、いつの間にか読む暇もなく忘れてしまう。

こんなダメダメな私はいったいどうすればいいのかと思って調べていたのですが、ある日次のようなブログ記事を見つけました。 そうだ、こんな簡単なことを忘れていた。 LLVMを使っているとても身近なコンパイラがあるじゃないか! 私は慌ててこのブログの人と同じことをしてみました (本記事はLLVMがインストールされており、LLVMツールのパスが通っていることを前提とします。私の手元では/usr/local/opt/llvm/bin/でした)。

 $ cat > test.c <<EOF
int main() {
  return 42;
}
EOF
 $ clang -S -emit-llvm -O2 test.c
 $ lli test.ll
 $ echo $?
42

これなら私にもできる! 高ぶる気持ちを抑えて実行ファイル生成まで試してみます。

 $ llc test.ll
 $ gcc test.s -o test
 $ ./test
 $ echo $?
42

実行ファイルを生成し、実行することができました。

clang -S -emit-llvmが生成したファイル、test.llをおそるおそる覗いてみます。

test.ll

; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"

; Function Attrs: norecurse nounwind readnone ssp uwtable
define i32 @main() #0 {
  ret i32 42
}

attributes #0 = { norecurse nounwind readnone ssp uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.0.0 (clang-800.0.42.1)"}

うーん、これはさっぱりです。 とりあえずコメントなどいらなさそうな物を削ってみて、どれだけ消しても動くか試してみます。

test.ll

source_filename = "test.c"

define i32 @main() #0 {
  ret i32 42
}

attributes #0 = { norecurse nounwind readnone ssp uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3" "unsafe-fp-math"="false" "use-soft-float"="false" }
 $ lli test.ll; echo $?
42

動きます。 #0と書いてあるのがよく分からないですが消してみます。

test.ll

define i32 @main() {
  ret i32 42
}
 $ lli test.ll; echo $?
42

動きました。なんだ、source_filenameもなくても動くんですね…

改めて最も小さくなったLLVM IR (Intermediate representation, 中間表現) のコードを眺めてみます。

test.ll

define i32 @main() {
  ret i32 42
}

defineで関数を定義しているのかな、retreturnだろうな、i32は32bit intなんだろなと、容易に想像が付きます。 こんな小さいコードなら、手で書くこともできそうです。 そうです、すでにdefine, ret, i32といったキーワードを理解し始めているのです。

ここまでやったことはとても簡単で、しかもすぐに試せることです。 とても小さなCのコードを書き、clang -S -emit-llvmLLVM IRファイルを生成し、llillcといったコマンドで実行できることを確かめながら、ファイルを削って本当に必要な部分を探しただけです。 LLVMのリファレンスは全く見なくても、Cのどういうコードがどういう命令列になるかはclangが教えてくれるのです。

この方法なら、長いリファレンスや入門チュートリアルを読まなくても、一歩ずつ着実にLLVM IRを学んでいけるのです。

きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~

きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~

  • 作者:柏木 餅子,風薬
  • 発売日: 2013/06/21
  • メディア: 単行本(ソフトカバー)

基本的な命令を学ぼう

int main()と書くとdefine i32 @main()となることや、return 42ret i32 42となることはわかりました。 しかしこれだけでは何もできないので、いくつか基本的な文を変換してみてLLVM IRがどうなるかを調べてみましょう。

まず、変数の宣言を調べてみます。 次のようなコードを書いてみます。

test.c

int main() {
  char c;
  int i;
  long l;
}

これのLLVM IRを見てみます。-O0で最適化されないように気をつけます。

 $ clang -S -emit-llvm -O0 test.c
 $ cat test.ll

コメントやattributesなどを削除したら次のようになります。

test.ll

define i32 @main() {
  %1 = alloca i8, align 1
  %2 = alloca i32, align 4
  %3 = alloca i64, align 8
  ret i32 0
}

実際このファイルは実行できます。

 $ lli test.ll

一行ずつ見比べると、chari8, inti32, longi64に対応していることがわかります。 alignはアドレスのアラインメントを表しており、変数のアドレスがこの数字の倍数であることが保証されます。

次に代入を調べます。 先程のコードに代入を追加して調べてみます。

test.c

int main() {
  char c;
  int i;
  long l;
  c = 'a';
  i = 72;
  l = 123456789012345;
}

このコードのLLVM IRは次のようになります。

test.ll

define i32 @main() {
  %1 = alloca i8, align 1
  %2 = alloca i32, align 4
  %3 = alloca i64, align 8
  store i8 97, i8* %1, align 1
  store i32 72, i32* %2, align 4
  store i64 123456789012345, i64* %3, align 8
  ret i32 0
}

store命令となりました。 第一引数が代入したい値で、第二引数が代入先のアドレスとなるようです。

少しコードを変えてみます。

test.c

int main() {
  char a, b;
  a = 32;
  b = a;
}

このコードのLLVM IRは次のようになります。

test.ll

define i32 @main() {
  %1 = alloca i8, align 1
  %2 = alloca i8, align 1
  store i8 32, i8* %1, align 1
  %3 = load i8, i8* %1, align 1
  store i8 %3, i8* %2, align 1
  ret i32 0
}

%132を保存し、%1の値をload命令で取り出して%3とし、それを%2storeしています。

足し算や引き算するとどうなるでしょうか。int同士の足し引きを見てみます。

test.c

int main() {
  int a, b, c;
  a = 32;
  b = a + 24;
  c = b - a;
}

test.ll

define i32 @main() {
  ; int a, b, c
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  %3 = alloca i32, align 4

  ; a = 32
  store i32 32, i32* %1, align 4

  ; b = a + 24
  %4 = load i32, i32* %1, align 4
  %5 = add nsw i32 %4, 24
  store i32 %5, i32* %2, align 4

  ; c = b - a
  %6 = load i32, i32* %2, align 4
  %7 = load i32, i32* %1, align 4
  %8 = sub nsw i32 %6, %7
  store i32 %8, i32* %3, align 4

  ret i32 0
}

やはり足し算はadd命令, 引き算はsub命令となりました。 nswは理解を後回しして良いでしょう。

後は標準出力が気になります。Hello worldLLVM IRを見てみます。

test.c

#include <stdio.h>
int main() {
  printf("Hello, world!\n");
}

test.ll

@.str = private unnamed_addr constant [15 x i8] c"Hello, world!\0A\00", align 1

define i32 @main() {
  %1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...)

declare命令でprintfを使うことを宣言し、call命令で呼び出しを行っています。 printfではなくてputcharを使ってみます。

test.c

#include <stdio.h>
int main() {
  char c;
  c = 'a';
  putchar(c);
}

test.ll

define i32 @main() {
  %1 = alloca i8, align 1
  store i8 97, i8* %1, align 1
  %2 = load i8, i8* %1, align 1
  %3 = sext i8 %2 to i32
  %4 = call i32 @putchar(i32 %3)
  ret i32 0
}

declare i32 @putchar(i32)

よく見ると、loadしてputcharするだけではなくて、sext命令が入っています。 そういえばputcharの引数はcharではなくてintでしたね。 sextはsigned extendの略で、型の変換を行う命令です。

Brainf**kのコンパイラを作ってみよう

宣言・代入・演算そして関数と、とても基本的なコードがLLVM IRでどのようになるかを観察してきました。 これだけでは実用的なプログラミング言語コンパイラを作るには知識が足りないのですが、プリミティブな言語の処理系なら作れる気がしてきました。

プリミティブな言語…といえばBrainf**kですよね。 私はこの言語が大好きです。 言語処理系のHello worldと言ってもいいのではないでしょうか。 先ほど紹介した記事でも、Brainf**kのコンパイラを書いています。 目的が同じですが、そっくり真似にならないようにちら見程度にして、自分の手で書いてみようと思います。

まずはBrainf**kのデータ領域の用意をします。

test.c

#include <stdlib.h>

int main() {
  char* data = (char*)calloc(30000, sizeof(char));
  char* ptr = data;
  free(data);
}

このLLVM IRを見てみましょう。

test.ll

define i32 @main() {
  %1 = alloca i8*, align 8
  %2 = alloca i8*, align 8

  %3 = call i8* @calloc(i64 30000, i64 1)
  store i8* %3, i8** %1, align 8

  %4 = load i8*, i8** %1, align 8
  store i8* %4, i8** %2, align 8

  %5 = load i8*, i8** %1, align 8
  call void @free(i8* %5)
  ret i32 0
}

declare i8* @calloc(i64, i64)

declare void @free(i8*)

i8*型の変数dataptrを作り、callocの結果を代入しています。

次に、ポインターの移動>とインクリメント+に対応するCのコードを、LLVM IRで見てみます。

test.ll

  ; >   ++ptr
  %5 = load i8*, i8** %2, align 8
  %6 = getelementptr inbounds i8, i8* %5, i32 1
  store i8* %6, i8** %2, align 8

  ; +   ++*ptr
  %7 = load i8*, i8** %2, align 8
  %8 = load i8, i8* %7, align 1
  %9 = add i8 %8, 1
  store i8 %9, i8* %7, align 1

getelementptrという新しい命令が出てきましたね。 意味は後で調べるとして、今はこういうものだと思っておきましょう。 ++*ptrは、ptrの値を%7に入れて、pointer dereferenceして%8に入れて、1足して%9に入れて、それを%7に代入する、と一行ずつ意味を読み取りやすいですね。

あとBrainf**kの.命令に対応するputchar(*ptr);のコードを見ておきます。

test.ll

  ; .  putchar(*ptr)
  %5 = load i8*, i8** %2, align 8
  %6 = load i8, i8* %5, align 1
  %7 = sext i8 %6 to i32
  %8 = call i32 @putchar(i32 %7)

そうでした、sext (signed extend) 命令でi32に変換する必要がありましたね。

Brainf**kの>, +, .に対応するLLVM IRコードが分かりました。<, -のコードも容易に想像がつきます。 ようやく道具が揃ったので、Brainf**kのLLVM IRコンパイラを書いてみます。

bf2llvm.c

#include <stdio.h>
#include <stdlib.h>

void emit_header() {
  printf("define i32 @main() {\n");
  printf("  %%data = alloca i8*, align 8\n");
  printf("  %%ptr = alloca i8*, align 8\n");
  printf("  %%data_ptr = call i8* @calloc(i64 30000, i64 1)\n");
  printf("  store i8* %%data_ptr, i8** %%data, align 8\n");
  printf("  store i8* %%data_ptr, i8** %%ptr, align 8\n");
}

int idx = 1;
void emit_move_ptr(int diff) {
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = getelementptr inbounds i8, i8* %%%d, i32 %d\n", idx + 1, idx, diff);
  printf("  store i8* %%%d, i8** %%ptr, align 8\n", idx + 1);
  idx += 2;
}

void emit_add(int diff) {
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = load i8, i8* %%%d, align 1\n", idx + 1, idx);
  printf("  %%%d = add i8 %%%d, %d\n", idx + 2, idx + 1, diff);
  printf("  store i8 %%%d, i8* %%%d, align 1\n", idx + 2, idx);
  idx += 3;
}

void emit_put() {
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = load i8, i8* %%%d, align 1\n", idx + 1, idx);
  printf("  %%%d = sext i8 %%%d to i32\n", idx + 2, idx + 1);
  printf("  %%%d = call i32 @putchar(i32 %%%d)\n", idx + 3, idx + 2);
  idx += 4;
}

void emit_footer() {
  printf("  %%%d = load i8*, i8** %%data, align 8\n", idx);
  printf("  call void @free(i8* %%%d)\n", idx);
  printf("  ret i32 0\n");
  printf("}\n\n");
  printf("declare i8* @calloc(i64, i64)\n\n");
  printf("declare void @free(i8*)\n\n");
  printf("declare i32 @putchar(i32)\n");
}

int main() {
  char c;
  emit_header();
  while ((c = getchar()) != EOF) {
    switch (c) {
      case '>': emit_move_ptr(1); break;
      case '<': emit_move_ptr(-1); break;
      case '+': emit_add(1); break;
      case '-': emit_add(-1); break;
      case '.': emit_put(); break;
    }
  }
  emit_footer();
  return 0;
}

動かしてみます。

 $ gcc bf2llvm.c -o bf2llvm
 $ echo "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++." | ./bf2llvm
define i32 @main() {
  %data = alloca i8*, align 8
  %ptr = alloca i8*, align 8
  %data_ptr = call i8* @calloc(i64 30000, i64 1)
  store i8* %data_ptr, i8** %data, align 8
  store i8* %data_ptr, i8** %ptr, align 8
  %1 = load i8*, i8** %ptr, align 8
  %2 = load i8, i8* %1, align 1
  %3 = add i8 %2, 1
  store i8 %3, i8* %1, align 1
  ; 略
  store i8 %195, i8* %193, align 1
  %196 = load i8*, i8** %ptr, align 8
  %197 = load i8, i8* %196, align 1
  %198 = sext i8 %197 to i32
  %199 = call i32 @putchar(i32 %198)
  %200 = load i8*, i8** %data, align 8
  call void @free(i8* %200)
  ret i32 0
}

declare i8* @calloc(i64, i64)

declare void @free(i8*)

declare i32 @putchar(i32)
 $ echo "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++." | ./bf2llvm | lli
A

やった! これでは+.しか試してないので他の命令も使ってみます。

 $ echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.+++++++++++++++++++++++++++++.+++++++..+++.>++++++++++++++++++++++++++++++++.<++++++++.--------.+++.------.--------.>+." | ./bf2llvm | lli
Hello world!

動いてそうですね! 最初の峠は超えたようです!

ループに対応しよう

Brainf**kの[, ]に対応しましょう。 まずはCで書いてclangLLVM IRを調べてみます。 [while (*ptr) {に、]}に対応します。 以下のプログラムは、Brainf**kの++[>++<-]>.に対応するコードで、実行したら"\x04"を出力します。

test.c

#include <stdio.h>
#include <stdlib.h>

int main() {
  char* data = (char*)calloc(30000, sizeof(char));;
  char* ptr = data;
  ++*ptr;
  ++*ptr;
  while (*ptr) {
    ++ptr;
    ++*ptr;
    ++*ptr;
    --ptr;
    --*ptr;
  }
  ++ptr;
  putchar(*ptr);
  free(data);
}

LLVM IRは次のようになりました。

test.ll

define i32 @main() {
  %1 = alloca i32, align 4
  %2 = alloca i8*, align 8
  %3 = alloca i8*, align 8
  store i32 0, i32* %1, align 4
  %4 = call i8* @calloc(i64 30000, i64 1)
  ; 略
  store i8 %11, i8* %9, align 1
  br label %12

; <label>:12                                      ; preds = %16, %0
  %13 = load i8*, i8** %3, align 8
  %14 = load i8, i8* %13, align 1
  %15 = icmp ne i8 %14, 0
  br i1 %15, label %16, label %30

; <label>:16                                      ; preds = %12
  %17 = load i8*, i8** %3, align 8
  %18 = getelementptr inbounds i8, i8* %17, i32 1
  store i8* %18, i8** %3, align 8
  ; 略
  store i8 %29, i8* %27, align 1
  br label %12

; <label>:30                                      ; preds = %12
  %31 = load i8*, i8** %3, align 8
  ; 略
  ret i32 %38
}

declare i8* @calloc(i64, i64)

declare i32 @putchar(i32)

declare void @free(i8*)

while文のジャンプにbr (branch) 命令が、条件文に対応する場所はicmp (integer compare) が使われていることがわかります。 ; <label>:12は、見ての通りラベルで、br label %12でそこにジャンプしているのだと想像がつきます。 次のようにラベルを読みやすく書くこともできます。

  br label %while_cond0

while_cond0:
  ; 略
  br i1 %14, label %while_body0, label %while_end0

while_body0:
  ; 略
  br label %while_cond0

while_end0:

while文の出力の仕方が分かったので、コンパイラの続きを書いてみます。

bf2llvm.c

void emit_while_start(int while_index) {
  printf("  br label %%while_cond%d\n", while_index);
  printf("while_cond%d:\n", while_index);
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = load i8, i8* %%%d, align 1\n", idx + 1, idx);
  printf("  %%%d = icmp ne i8 %%%d, 0\n", idx + 2, idx + 1);
  printf("  br i1 %%%d, label %%while_body%d, label %%while_end%d\n", idx + 2, while_index, while_index);
  printf("while_body%d:\n", while_index);
  idx += 3;
}

void emit_while_end(int while_index) {
  printf("  br label %%while_cond%d\n", while_index);
  printf("while_end%d:\n", while_index);
}

/* 略 */
int main() {
  char c;
  int while_index = 0;
  int while_indices[1000];
  int* while_index_ptr = while_indices;
  emit_header();
  while ((c = getchar()) != EOF) {
    switch (c) {
      /* 略 */
      case '[': emit_while_start(*while_index_ptr++ = while_index++); break;
      case ']': emit_while_end(*--while_index_ptr); break;
    }
  }
  emit_footer();
  return 0;
}

試してみましょう。

 $ gcc bf2llvm.c -o bf2llvm
 $ echo "+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.------------.<++++++++.--------.+++.------.--------.>+." | ./bf2llvm | lli
Hello, world!

動いてそうですね! もう少しややこしいコードを動かしてみます。

 $ cat fibonacci.bf
>>+++++++++++[-<<++++>+++>>+<]>>+<++<<->>[>>>++++++++++<<[->+>-[>+>>]>[+[-<+>]>+
>>]<<<<<<]>>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]>[-]>>[>++++++[-<++++
++++>]<.<<+>+>[-]]<[<[->-<]++++++[->++++++++<]>.[-]]<<++++++[-<++++++++>]<.[-]<<
[-<+>]<<[->>+>+<<<]>>>[-<<<+>>>]<-[<<<<<.>.>>>>[-]]<[->+>+<<]>>[-<<+>>]<<<<[->>+
<<]>>>[-<<<+>>>]<<-]
 $ cat fibonacci.bf | ./bf2llvm | lli
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233
 $ cat prime.bf
++++++++++[->+++++>++++>+++<<<]>>++++>++>>+>+++<<<<<.>>>>>[<[-]++<[-]+>>>+[<[->>
+<<]<[->>>>+>+<<<<<]>>>>[-<<<<+>>>>]<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>[-<<<+>>>
]>[-]>[-<<+>+>]<<[->>+<<]+>[<[-]>[-]]<[<<<<<[-]>>>>>[-]]>[-]>[-]>[-]<<<<<<-<[->>
>+>+<<<<]>>>>[-<<<<+>>>>]<<<[->>>>+>+<<<<<]>>>>>[-<<<<<+>>>>>]<<<+[->>[->+>+<<]>
>[-<<+>>]+<[>-<<->[-]]>[<<<+<[-]>>>>[-]]<[-]<<<]>>[-]<[<<->>[-]]<[-]<<+<[->>>+>+
<<<<]>>>>[-<<<<+>>>>]>+++++++++++++<<[->>[->+>+<<]>>[-<<+>>]+<[>-<<->[-]]>[<<<+<
[-]>>>>[-]]<[-]<<<]>>[-]<[<<[-[-]]>>[-]]<[-]<<<+>>]<<<[<.>>>>>++++++++++<<[->+>-
[>+>>]>[+[-<+>]>+>>]<<<<<<]>[-<+>]>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<
<]>[-]>>[>++++++[-<++++++++>]<.<<+>+>[-]]<[<[->-<]++++++[->++++++++<]>.[-]]>[-]<
<<++++++[-<++++++++>]<.[-]<<<<<[-]]>>+]
 $ cat prime.bf | ./bf2llvm | lli
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251

Erik Bosmanさんという方が書いた、マンデルブロ集合のプログラムも動かすことができます。 出力は読者が確認してください。

基本構文に対応できてそれらを使った小さなコードが動いてしまえば、あらゆるプログラムが動く (とは言えメモリーの限界はありますが) というのはコンパイラの分野でコード書いていて楽しい所です。

最適化

LLVMにはLLVM IRレベルの命令列を最適化する機能があります。 LLVMをバックエンドに持つコンパイル言語は、LLVMの最適化の恩恵を受けられます。

LLVM IR命令列がどのように最適化されるかを調べるには、optコマンドを使うとよいでしょう。

 $ cat hello.bf 
+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.------------.<++++++++.--------.+++.------.--------.>+.
 $ cat hello.bf | ./bf2llvm | opt -S -O3
; ModuleID = '<stdin>'
source_filename = "<stdin>"

; Function Attrs: nounwind
define i32 @main() local_unnamed_addr #0 {
while_end0:
  %0 = tail call i32 @putchar(i32 72)
  %1 = tail call i32 @putchar(i32 101)
  %2 = tail call i32 @putchar(i32 108)
  %3 = tail call i32 @putchar(i32 108)
  %4 = tail call i32 @putchar(i32 111)
  %5 = tail call i32 @putchar(i32 44)
  %6 = tail call i32 @putchar(i32 32)
  %7 = tail call i32 @putchar(i32 119)
  %8 = tail call i32 @putchar(i32 111)
  %9 = tail call i32 @putchar(i32 114)
  %10 = tail call i32 @putchar(i32 108)
  %11 = tail call i32 @putchar(i32 100)
  %12 = tail call i32 @putchar(i32 33)
  ret i32 0
}

; Function Attrs: nounwind
declare i32 @putchar(i32) local_unnamed_addr #0

attributes #0 = { nounwind }

なんと、IO以外は全て計算されてしまいました! もともと配列を確保してその上で計算していたはずが、最適化によってそれも消えてしまいました。

もう少し複雑な、マンデルブロ集合のスクリプトで実行時間を比較してみましょう。

 $ time sh -c 'cat mandelbrot.b | ./bf2llvm | lli > /dev/null'
        6.34 real         6.27 user         0.04 sys
 $ time sh -c 'cat mandelbrot.b | ./bf2llvm | opt -S -O3 | lli > /dev/null'
        3.55 real         3.50 user         0.05 sys

LLVMの最適化によって約40%も高速化しました (Hello worldほど単純ではないので、配列ごと消えるということはありません)。 コンパイルして実行ファイルを作ってみます。

 $ cat mandelbrot.b | ./bf2llvm | opt -S -O3 > mandelbrot.ll
 $ llc mandelbrot.ll
 $ gcc mandelbrot.s -o mandelbrot
 $ time ./mandelbrot > /dev/null
        0.98 real         0.97 user         0.00 sys

はい。すごく速いですね。 手でBrainf**kのインタープリタを書いて最適化しようとしても、小手先の改善の組み合わせでLLVMレベルの最適化するのは難しいでしょう。 まずは素朴に命令列を吐いて、最適化をおまかせしてしまえば速度が出るとてもうれしいですね。 私のようにアセンブリの分からない人でも、LLVM IRを正しく吐くことさえできれば実行ファイル生成まで面倒見てくれるのはLLVMの素晴らしいところですね。

まとめ

LLVMの最初の一歩はLLVM IRの命令と親しむところにあります。 しかし多くのチュートリアルでは、難しい言語をC++でパースして構文木を作ってIRを吐いてという説明から始まります。 できる人にはできるのでしょうが、私には理解が追いつきませんでした。 LLVM IRを初めて触った人が、いきなりIRBuilderからコード生成できるのでしょうか。 LLVM素人がいきなりIRBuilderのメソッド一覧を眺めて、これを使えばいいなみたいにすぐに分かるものなのでしょうか。

そんな人にとって、clangは良い先生だと思います。 CのコードがどのようなLLVM IRの命令列になるか教えてくれます。 CとLLVM IRを見比べながら、自分でも命令列を吐くコードを書いてみて、徐々に分かっていくものだと思います。 それはIRBuilderを使ったコードに比べたら泥臭いコードかもしれませんが、入門の壁を超える大事な一歩だと思います。

この記事は、実際に私自身が一歩ずつ理解を深めながら、同時並行して書き進めてきました。 私はLLVMについてはド素人で、Kaleidoscopeレベルにすら到達していません。

そんな私だからこそ、この記事を書こうと思った時には今の私に書ける「Kaleidoscopeの手前のLLVM入門記事にしよう」という思いがありました。 LLVMを触ってみたいけど、最初の壁が超えられない、そういった人に届けばいいなと思います。

いくつかの命令についてあまり説明せずに通り過ぎたところがありました。 命令の正確な意味や引数などの理解を深めるためには、言語マニュアルを参照してください。 なお、用語や記述には気をつけて書いていますが、誤りがあればご指摘ください。

LLVMのページを開いた時、命令の説明書をぼーっと眺めている時、このツールは私に使えるものなのだろうかという不安がありました。 今の気持ちは全く違います。 実際に触って試行錯誤したら、コツが分かってきました。 ようやくLLVMの世界の入り口に立った気持ちです。 IRの命令の雰囲気が掴めてきたので、次はIRBuilderを使ったコード生成や、実用的な言語のコンパイラ作成に挑戦してみようと思います。 今後も理解を深め、コンパイラ技術の勉強を続けていこうと思います。

続きです。 itchyny.hatenablog.com

ソースコード

bf2llvm.c

/*
 * Brainf**k -> LLVM IR Compiler
 *  $ gcc bf2llvm.c -o bf2llvm
 *  $ echo "+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.\
            >-.------------.<++++++++.--------.+++.------.--------.>+." | \
            ./bf2llvm | opt -S -O3 | lli
 */
#include <stdio.h>
#include <stdlib.h>

void emit_header() {
  printf("define i32 @main() {\n");
  printf("  %%data = alloca i8*, align 8\n");
  printf("  %%ptr = alloca i8*, align 8\n");
  printf("  %%data_ptr = call i8* @calloc(i64 30000, i64 1)\n");
  printf("  store i8* %%data_ptr, i8** %%data, align 8\n");
  printf("  store i8* %%data_ptr, i8** %%ptr, align 8\n");
}

int idx = 1;
void emit_move_ptr(int diff) {
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = getelementptr inbounds i8, i8* %%%d, i32 %d\n", idx + 1, idx, diff);
  printf("  store i8* %%%d, i8** %%ptr, align 8\n", idx + 1);
  idx += 2;
}

void emit_add(int diff) {
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = load i8, i8* %%%d, align 1\n", idx + 1, idx);
  printf("  %%%d = add i8 %%%d, %d\n", idx + 2, idx + 1, diff);
  printf("  store i8 %%%d, i8* %%%d, align 1\n", idx + 2, idx);
  idx += 3;
}

void emit_put() {
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = load i8, i8* %%%d, align 1\n", idx + 1, idx);
  printf("  %%%d = sext i8 %%%d to i32\n", idx + 2, idx + 1);
  printf("  %%%d = call i32 @putchar(i32 %%%d)\n", idx + 3, idx + 2);
  idx += 4;
}

void emit_get() {
  printf("  %%%d = call i32 @getchar()\n", idx);
  printf("  %%%d = trunc i32 %%%d to i8\n", idx + 1, idx);
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx + 2);
  printf("  store i8 %%%d, i8* %%%d, align 1\n", idx + 1, idx + 2);
  idx += 3;
}

void emit_while_start(int while_index) {
  printf("  br label %%while_cond%d\n", while_index);
  printf("while_cond%d:\n", while_index);
  printf("  %%%d = load i8*, i8** %%ptr, align 8\n", idx);
  printf("  %%%d = load i8, i8* %%%d, align 1\n", idx + 1, idx);
  printf("  %%%d = icmp ne i8 %%%d, 0\n", idx + 2, idx + 1);
  printf("  br i1 %%%d, label %%while_body%d, label %%while_end%d\n", idx + 2, while_index, while_index);
  printf("while_body%d:\n", while_index);
  idx += 3;
}

void emit_while_end(int while_index) {
  printf("  br label %%while_cond%d\n", while_index);
  printf("while_end%d:\n", while_index);
}

void emit_footer() {
  printf("  %%%d = load i8*, i8** %%data, align 8\n", idx);
  printf("  call void @free(i8* %%%d)\n", idx);
  printf("  ret i32 0\n");
  printf("}\n\n");
  printf("declare i8* @calloc(i64, i64)\n\n");
  printf("declare void @free(i8*)\n\n");
  printf("declare i32 @putchar(i32)\n\n");
  printf("declare i32 @getchar()\n");
}

int main() {
  char c;
  int while_index = 0;
  int while_indices[1000];
  int* while_index_ptr = while_indices;
  emit_header();
  while ((c = getchar()) != EOF) {
    switch (c) {
      case '>': emit_move_ptr(1); break;
      case '<': emit_move_ptr(-1); break;
      case '+': emit_add(1); break;
      case '-': emit_add(-1); break;
      case '[': emit_while_start(*while_index_ptr++ = while_index++); break;
      case ']': emit_while_end(*--while_index_ptr); break;
      case '.': emit_put(); break;
      case ',': emit_get(); break;
    }
  }
  emit_footer();
  return 0;
}

二週間で簡単なインタープリタ言語を実装してみた (日記)

私は昔から言語処理系に興味があり、自分で言語を作ることを夢見てきました。 しかし、いざ言語を実装しようと思って言語処理系に関する本を読んでも何から手を付けていいか分からず、アセンブラもまともに読めないまま、数年が経ってしまいました。 大学時代は情報系ではなかったため、コンパイラの実験がある情報系の学科のカリキュラムを羨ましく思い、情報系の授業の教科書を手にとって見ても読む気が起きず、自分に作れるのは所詮、構文木をちょこっといじって変換するレベルのもの (例えばsjspなど) にとどまっていました。

そんな中、去年のRebuild.fmで、とても感銘を受けた回がありました。 LLVMのlinkerであるLLDを開発されているrui314さんの回です。 rebuild.fm セルフコンパイルできるC言語コンパイラを実装するという話のなかで、インクリメンタルに開発する重要性について話をされています。

qiita.com

この回を聞いてやっと、コンパイラの書き方がわかった気がしました。 コンパイラに関する本を読んでも実装の進め方がわからなかったのは、コンパイルフェーズごとに章に分かれていたからで、正規言語の難しい話を読んでは本を投げ出していたのです。 Rebuild.fmのrui314さんのお話を聞いて、まず最初は小さい言語をとにかくパースからコード生成まで通して動くようにして、そこから徐々に文法を増やしていくというインクリメンタルなアプローチが有力なのだとやっとわかったのです。 ここで最初の言語は、関数もforループも演算子もない、とてもプリミティブな文法だけもつ言語から始めるのです。 コンパイラの教科書を読んでもさっぱりわからなかった実装の進め方が、ようやくわかった瞬間でした。

最近、まつもとゆきひろさんの『言語のしくみ』が出版されたこともあり、言語実装の熱が高まっているように感じます。 私は今年は言語実装の年にしようと思っていて、その第一歩としてインタープリタ言語を書いてみました。 まずやったことは、mrubyやLuaなどの実装を手元に引っ張ってきて、ctagsを打って数日読んでみました。

Luaのファイル構成は分かりやすいので読みやすいです。mrubyはmrbgems/mruby-compiler/coreやsrc/vm.cなどを中心に見ていきました。ざっくり雰囲気を掴むくらいの気持ちで読みました。 あとはひたすら実装を進めていきました。

つまり参考にしたものはmrubyとLuaです。 書籍としては以下の本を挙げておきます (もうちょっとやわらかいインタープリタ実装の本ある気がするけど知らないです)。

大学時代に買ったけど当時はあまり理解できず、最近開いてみてようやく理解し始めたコンパイラの本。 この本だけ読んでてもよく分からないけど、自分でVMを書いてみてから改めてこの本を読んでみたら、納得する部分がたくさんありました (特に関数のコード生成について)。 大学で使われている教科書で少し硬い。

情報系教科書シリーズ コンパイラ

情報系教科書シリーズ コンパイラ

Direct threadingなどmrubyで使われている技術がよく分かる本 (すみません、最初の方をちら見しただけでちゃんとは読んではないです…)。

rui314先生をリスペクトしているので、私も日記形式でお送りしてみます。

1月3日

実家から戻り、インタープリタ言語を作る意識を高める。 ディレクトリを作り、yaccファイルを用意する。

1月7日

mrubyの構文木の実装を参考にして、ASTを全てnode*で実装することにする。

typedef struct node {
  struct node *car, *cdr;
} node;

とりあえずノードを追加するたびにmallocするような形で実装を進める。 文法は四則演算のみ。

1月8日

構文木のためのメモリープールを実装する。 ASTは全てnode*で表すことにしたので、この構造体のみ考えておけば良いのは楽。

四則演算のみの言語に対して、コード生成と実行器を実装する。 実装が簡単なスタックマシンで作る。

今ある文法は

  • 式 (expression) が並んだものがprogram
  • expressionは四則演算が使える
  • リテラルは整数 (long) または浮動小数点数 (double)
enum OP_CODE {
  OP_ADD,
  OP_MINUS,
  OP_TIMES,
  OP_DIVIDE,
  OP_LOAD_LONG,
  OP_LOAD_DOUBLE,
  OP_PRINT_POP,
};

OP_PRINT_POPはちょっとダサい…

1月9日

コードが読みやすいようにいくつか変数名をリネーム。 変数を作り始めるが難しくて散歩に出かける。

夜、変数を実装。新たに追加した文法は

  • 変数に代入できる
  • 四則演算のなかで変数を使える
  • print文
statement         : IDENTIFIER EQ expression
                    {
                      $$ = cons(nint(NODE_ASSIGN), cons($1, $3));
                    }
                  | PRINT expression
                    {
                      $$ = cons(nint(NODE_PRINT), $2);
                    }
                  ;

OP_PRINT_POPを削除してOP_ASSIGN, OP_PRINT, OP_LOAD_IDENTを追加。

 enum OP_CODE {
+  OP_ASSIGN,
+  OP_PRINT,
   OP_ADD,
   OP_MINUS,
   OP_TIMES,
   OP_DIVIDE,
   OP_LOAD_LONG,
   OP_LOAD_DOUBLE,
-  OP_PRINT_POP,
+  OP_LOAD_IDENT,
 };

OP_ASSIGNOP_LOAD_IDENTのoperandは、変数配列のindexが入っているので、実行器の雰囲気はこんな感じ。

+      case OP_ASSIGN:
+        e->variables[GET_ARG_A(e->codes[i])].value = e->stack[--e->stackidx];
+        break;
       case OP_ADD: BINARY_OP(+); break;
@@ -172,6 +214,9 @@ static void execute_codes(env* e) {
         break;
+      case OP_LOAD_IDENT:
+        e->stack[e->stackidx++] = e->variables[offset - GET_ARG_A(e->codes[i])].value;
+        break;

booleanリテラルを実装する。まだ演算はない。

1月11日

簡単なテストケースを書く。

foo = 3 * 4 / 5 + 6.7 * 7
bar = foo * 3.1 + foo / 7.2
print foo + bar

このスクリプトを実行すると207.281666667と表示される。 Python 2のコンソールで確かめながら実行結果を確かめる。

if文を実装する。Vim scriptに倣って、if … endifにする。 if文の実装に伴って、OP_JMP_NOTを作る。 各ステートメントの命令数をカウントしないといけないことに気が付き、codegen関数の返り値をvoidからuint16_tにする。

1月12日

else文を実装する。 無条件にジャンプするOP_JMPを作る。

if true
  a = 10
  print a
else
  b = 11
  print b
endif

このスクリプトは次のようなコードに変換されて実行される。

bool true
jmp_ifnot 5
long 10
let 0
load 0
print
jmp 4
long 11
let 1
load 1
print

for文の中にprogram counterをincrementするところがあるので、jmp_ifnotのoperandが1少ないように見えるがこれで正しい。

elseif文を実装する。 構文木を構築するときに新しいif文にしてしまうことで、コード生成を触らなくても実装できてしまうことに気がつく。

デバッグしやすいように、構文木LISPのような形式で表示できるようにする。

1月13日

while文を実装する。 これまでoperandはuint16_tだと思っていたが、これではジャンプ命令で負の値を指定できないという初歩的なミスに気がつく。 とりあえず暫定対処でOP_JMP_BACKを追加する。

>=, ==, <= など、数値の比較を行う演算子を実装する。

&&, || を実装する。これらは四則演算などの他の二項演算子とは異なり、右辺を評価しないことがある。 if文を作ったときに追加したOP_JMP_NOTは評価値をスタックからpopしてしまうため、仕方なしにOP_JMP_NOT_KEEPを作る。

if文とwhile文が実装されたので、なんとなく「言語らしさ」が出てきてかわいい。

1月14日

operandをuint16_tからint16_tに変更し、OP_JMP_BACKを削除する。 少しリファクタリングする。

最近は仕事が忙しくて進められていない。

1月16日

組み込み関数min(), max()を実装する。 関数の引数のASTを組み立てていて、ようやくnode*の良さが分かってくる。 関数の実装はべた書きでスタックを直接操作していて危ない。 関数呼び出しのoperandには、何番目の関数かというindexと、引数の数を指定するようにする。 ユーザー定義の関数の実装を妄想する。

1月17日

単項演算子 +, - を実装する。 組み込み関数abs()を実装する。

1月18日

endifとendwhileをendにする。RubyLuaっぽくてかわいい。

a = 10
while a >= 0
  print a
  if a > 5
    print 10
  else
    print 0
  end
  a = a - 1
end

ユーザー定義関数をとりあえずパースできるようにする。

+statement         : FUNC IDENTIFIER LPAREN fargs_opt RPAREN sep statements sep END
+                    {
+                      $$ = cons(nint(NODE_FUNCTION), cons($2, cons($4, $7)));
+                    }
+                  | RETURN expression
+                    {
+                      $$ = cons(nint(NODE_RETURN), $2);
+                    }

まだコード生成はどうすれば良いのかさっぱりわからない。

ファイルが大きくなってきたので分割する。

1月19日

simple virtual machineという意味でsvmと名付けていたが、support vector machineと紛らわしいので、minivmにrenameする。

ユーザー定義関数の実装に想いを馳せる。

1月21日

ユーザー定義関数のコード生成と実行器を実装する。 program counterをsave・unsaveするOP_SAVEPC, OP_UNSAVEPC、ローカル変数の領域を確保と解放を行うOP_ALLOC, OP_UNALLOCを実装する。 少し汚いけど、実行時には変数領域を逆さまに使うようにすると、確保した数を持たなくてよいので楽ということに気がつく。 関数の実行は、おおむね次のような流れになる。

  • 引数を順番に評価してスタックに積む
  • 現在のprogram counter (呼び出し前のpc) をスタックに積む
  • 関数呼び出しを行う
  • ローカル変数のための変数領域を確保
  • popして呼び出し前のpcをローカル変数に入れる
  • 引数をpopして後ろからローカル変数に入れていく
  • 関数の中身を評価する
  • return文で、ローカル変数の変数領域を解放し、呼び出し前のpcに戻す

しかし、関数の中からグローバル変数が見れないとか、再帰呼出しができない (関数自体はグローバル変数領域に確保されるが、関数の中身のコード生成時にローカル変数リストを見ているため) という問題に直面し、頭を抱える。昼寝する。

単項演算子 ! (not) を実装する。

1月22日

グローバル変数とローカル変数は全く性質が違うと悟る。 湯淺先生の本の関数のコード生成の節を読む。 ローカル変数に対するオペコード OP_LET_LOCAL, OP_LOAD_LOCAL_IDENT を追加し、グローバル変数とは区別するように実装し、ようやく関数の中からグローバル変数を参照したり、再帰呼び出しできるようになる。

a = 10
b = 20
func foo(x)
  b = 30
  return a * b + x
end

print foo(50)
print a
print b

このスクリプトが350, 10, 20と表示するようになる (昨日の時点ではグローバル変数領域を見れてなかったので、Undefined variable: aだった)。

フィボナッチ数を計算する (効率の悪い再帰呼び出しの) スクリプトが動き、とても興奮する。テストケースに追加する。

OP_UNALLOCOP_UNSAVEPCを削除してOP_RETに統一する。ローカル変数領域を明け渡すとともに、program counterを呼び出し元に戻す。

break文とcontinue文を実装する。 continueは簡単なのでまあいいとして、break文のジャンプ先がわからずに小一時間悩む。 ワンパスで生成までやっているので、while文の中身を生成しているときは飛び先のpcが分からない。 仕方なく、while文の先頭にwhile文の最後まで飛ぶジャンプ命令を追加する。

スタックの一番上をコピーして積むOP_DUPを追加して、 &&, ||を作ったときに追加したOP_JMP_IF_KEEPOP_JMP_NOT_KEEPを削除する。

パフォーマンス改善のために、即値を足したり引いたりするOP_IADDOP_IMINUSを追加する。 足し算は可換なので即値が左のときも適用できるはずだけど後回し。

そこそこコードを書けるようになったので、軽くスクリプトを書いてみたり速度比較をしてみる。 以下のスクリプトが動く。

func fib(n)
  if n <= 1
    return 1
  end
  return fib(n - 1) + fib(n - 2)
end

n = 0
while n < 38
  print fib(n)
  n = n + 1
end

このスクリプトを9.05sで実行できた。 これと全く同じことを行うスクリプトruby 2.1.9で9.10s, mruby 1.2.0で14.78s, Lua 5.2.4で12.83s, Python 2.7.12で26.30sかかった。 スクリプト言語としてそこそこの速度が出ている事がわかる。

なお、生成された中間コードは次のような感じになった。

long 3
let 0
jmp 20
ret 2
alloc 2
let_local 1
let_local 0
load_local 0
long 1
<=
jmp_ifnot 2
long 1
jmp -10
load_local 0
iminus 1
call 0 1
load_local 0
iminus 2
call 0 1
+
jmp -18
long 0
jmp -20
long 0
let 1
jmp 1
jmp 11
load 1
long 38
<
jmp_ifnot 7
load 1
call 0 1
print
load 1
iadd 1
let 1
jmp -11

こうやって改めて見てみると、やはり関数の最初にretがあったりwhile文の最初のjmpとかして意味不明かもしれない。 returnやbreakなど、生成時に本来飛びたいジャンプ先がわからずにこうなってしまった。 ワンパスで生成するのにこだわらなかったらもう少し素直なコードになるはずだけど、今回は深追いしないことにする。

これからやること

言語処理系としてやらないといけないことは、まだまだたくさんあります。 文字列や配列、辞書なども作りたいし、型とか関数引数の数のチェックなどをスキップしているので、誤った文法のスクリプトを流すと簡単にセグフォしてしまいます… 流石にそういうのは直したい。 構文エラー時のメッセージも雑すぎるのでなんとかしたいと思っています。 今の実装だとおそらくクロージャが作れないので、もう少しスコープをきちんと扱えるようになってから考えたいです。

それっぽく動くものはできたけど色々と気に入らない部分はあるので、今回の実装はいったん捨てて、また1から書いていくと思います。 参考程度にGitHubリポジトリを置いておきます。

まとめ

二週間で初めてのインタープリタ言語を実装できたので、今年中にあと23個くらい言語処理系を書くと思います。 今回はスタックマシンで作りましたが、レジスタマシンのコード生成はどうすればいいのかよくわからないし、並行処理のコード生成とか検討もつきません。 LLVMバックエンドを使ってみたり、アセンブラを吐いてみたり、GCを実装してみたり、あるいはgoroutineスケジューラの実装も読んでみたりと、やりたい事が山積みです。 Scalaのwebアプリケーションを運用している以上はJVMについて詳しくなりたいという思いもあります。

その最初のステップとして、簡単なインタープリタ言語を実装してみました。 とりあえず最初の一歩を踏み出せてよかったです。 小さい構文から始めて、少しずつ構文を足していくというやり方は、言語処理系を作る上で有用だと思います。 よく考えてみればそりゃそうかという方法ですが、rui314さんのお話を始めて聞いたときは目から鱗が落ちる思いがしました。

ASTにmruby方式 (LISPのようにcar cdrで表す) を採用したことは、良い面と悪い面がありました。 メモリー管理は1つの構造体について集中しておけばいいので楽です。 関数の引数部分のように、まさにリスト構造になっている部分は配列にするよりも、consで表すほうが楽だと思います。 悪い面としては、コード生成時にcarやcdrが何を表しているのかよく分からなくなるという問題があります。 パーサーの対応する部分を見れば分かるのですが、生成部分のコードだけを見ていたら何も分かりません。 次に書く言語では、この方式は使わずに実装してみたいと思います。

言語実装の楽しみは、バグっていたら全く動かないし、きちんと実装できたら、その文法で書けるあらゆるコード、あらゆるアルゴリズムが動くようになるというところにあると思います。あるところまで書いたら可能性が一気に広がる楽しさというのは昔Tweetしたことがあります。 この思いは今でも変わっていません。

言語処理系の分野は広大で、いくらでも学ぶ楽しみが広がっています。 いくらやっても学ぶことがあり、しかも実装してみて動くとすごく楽しいというのは、プログラマの趣味としてはうってつけではないでしょうか (趣味レベルに作る程度だと、本業で研究されているレベルまではいくらやっても到達しないままかもしれませんが…)。 実装が公開されている言語もいくつもあり、素晴らしい教材がたくさん見つかります。 第一歩として、スタックマシンのインタープリタ言語を実装してみました。 レジスタマシンやネイティブコード生成、LLVMバックエンドなど、色々な方式を試しながら、言語処理系作りを楽しみたいと思います。

2016年を振り返って

会社は二年目に入り業務にも慣れ、ある程度まとまった仕事を任せられるようになりました。 携わっているサービスのコードに詳しくなり、リファクタリングの方向性を示して改善を進めてきました。 難しい障害も乗り越えながら、引き継いだ手綱を何とか制御できるようになってきたという所感です。

今年は18記事書きました。特に反響の大きかったエントリーは次の3つの記事でした。 内容の方向性もバラバラであまり何したいかよく分からなくなっていますね。どういう技術を学んでいくか悩んでいた一年だったと思います。ブログには書いていませんが、Vimソースコードをいじったりmrubyのコードを読み込んだりしていた時期もありました。

一年に一つ言語を学べという教えを元に次に学ぶべき言語を模索するために様々な言語を触ってみるというチャレンジに取り組んだのですが、その後とくに新しい言語を触れていないのは反省しています。 少しRustのチュートリアルを進めましたが、実際に自分で何かを作ってみるところまでは進めませんでした。 観測範囲ではRustは徐々に人気が高まっているので来年には真面目に手を付けて扱えるようになりたいものです。

最近はVM関連の関心が高まっていて、インタープリタ言語の実装を調べているところです。 ソースコードだけを読んでいるとなかなか背景の技術まで理解できなかったりするのですが、mrubyに関しては最近出版された以下の本が参考になります。

mrubyのmrb_vm_execにあるoptableあたりのコードを最初見た時は何だこれはと思ったのですが、この書籍を読んでようやく意味がわかりました。 調べてみるとVMの実装では有名なテクニックらしいです。 来年は自分で言語を実装しながら色々と学びたいと思います。

Vimは今でももちろん毎日使っていますが、プラグインを制作したいという欲は沸かなくなっています。 既存のプラグインに対するissue報告に対処しながら、リファクタリングなどをのんびりやっていました。 特に、thumbnail.vimの大幅なリファクタリングは時間がかかりました。 thumbnail.vimは自主的に作った最初のプラグインですが、そのせいでコードは複雑に絡み合っており、冗長なtry catchや難解な再帰呼び出しを前に、長い間コードに手を付けられずにいました。 calendar.vimでの設計をベースに、絡み合ったコードを丁寧に解きほぐし、何とかきれいにまとめることができました。 設計のめちゃくちゃなコードを挙動を変えずにリファクタリングするのはとても大変ですね。

来年は、興味のあるVM実装周りの知識を深めながら手を動かして実装を進めていきたいと思っています。 また、良い言語だという予感はあるもののなかなかチュートリアルレベルを抜け出せていないRustを真面目に使ってCLIツールを作ってみたいと思っています。 HaskellScala、Go、JavaScriptそしてRustもスキルセットに揃うと、多くのシーンに柔軟に技術を選択できるのではないかなと思っています。

2016年もいい年でした。技術を深め研鑽しながら来年も頑張っていきましょう。

堤京介「終わらないよ。あきらめなければ、終わらない。俺は何度でも挑戦する。」

ef - a tale of memories.

珍しいSHA1ハッシュを追い求めて

SHA1ハッシュってあるだろう?」

放課後、いつものように情報処理室に行くと、高山先輩が嬉しそうな顔でそう言った。

「ええ、SHA1、ありますね」

SHA1って何桁か覚えているかい?」

「えっと…」

一年下の後輩、岡村が口を開いた。

「50桁くらいはありましたっけ…?」

先輩はパソコンに向かって何かを打ちはじめた。

現在、情報部の部員は三人しかいない。部長の高山先輩と、二年の自分と、後輩の岡村だ。いや、正確に言うと、先輩の学年にはもう少しいたのだが、もうほとんど部室に来ることはなくなってしまった。無理もない、この季節になると先輩たちは受験勉強で忙しくなる。

「例えば、こういうふうに… 適当なSHA1の長さを…」

echo -n | openssl sha1 | awk '{print length}'

部長だけは今も部活に来てこうやって色々なことを教えてくれている。本人曰く、普通に勉強したら受かるでしょ、らしい。そう言ってもあまり嫌味には聞こえないのが先輩のいいところだ。

「40だ。40桁だね」

「そうなんですね」

後輩の岡村は身を乗り出して先輩の端末操作を観察している。

「正確には160桁ですよね…」

僕は思わず口を開いた。

「そう、桁というより、160ビット、それを16進数で表現するから…」

先輩はそう言いながらプリント用紙の山から一枚手にとって説明しはじめた。

「こうやって4ビットずつ区切って0からfで表現するから、160ビットのハッシュを40桁で表現できるんだ」

「なるほど〜」

岡村は指を折りながら2進数表現を確認している。岡村の理解が追いついていることを確認しながら先輩は口を開く。

「つまり、SHA1ハッシュは16文字40桁で表現される。つまり16の40乗種類考えられるわけだね。これはもちろん2の160乗と同じ」

そう言いながら先輩はプリント用紙に簡単な式を書く。

「えっと、0.3かけると… これくらいかな…」

16^{40} = 2^{160} \simeq 10^{48}

「10の48乗…たくさんありますね…」

「そうだね、そこそこ多い」

先輩はシャーペンを顔の前で振りながらこう言った。

「そこでだ、SHA1ハッシュで、全てが数字になることはあるだろうか」

全て数字になるSHA1ハッシュ

情報部の活動は、いつもこんな感じで始まる。 誰かが問題を持ち寄って、それを下校時刻までにそれぞれ解く。 今日は部長の出題というわけだ。

空文字のSHA1ハッシュは da39a3ee5e6b4b0d3255bfef95601890afd80709 だ (これはさっき先輩がやってみせたようにecho -n | openssl sha1すると表示される)。これにはdやらaやらeなど、沢山アルファベットが含まれている。 つまり、SHA1ハッシュの16進数表現が、全て数字になるような元メッセージを求めよという問題なのだ。

早速、自分も作業に取り掛かった。 まずどれくらい存在するかを考える。 16文字の中で10文字が数字なので… と言いながらブラウザーのアドレスバーに (10/16)^40 と押してエンターを押した。

\displaystyle \left(\frac{10}{16}\right)^{40} = 6.84 \cdots \times {10}^{-9}

10の-9乗か… しかしSHA1ハッシュから直接元のメッセージを求めるのはできないので虱潰しに調べるしかない…

そんなことを考えながら、プログラムを書き始めた。 メッセージは適当にアルファベットと数字から作って、とにかくsha1ハッシュを計算して条件にあっていれば表示するだけのプログラムだ。 後輩の岡村の様子を見ると、どうやらSHA1ハッシュのアルゴリズムを確認しているようだった。 部長はシャーペンで何やら数式を書いている。

自分のプログラムは、しばらくして次のように出力しはじめた。

gflyP 9708825594493053358052040804954710052563
OqmSX 5405673447021682949913714302263814023323
pcBsd 0830855821698284247230340812332913765683
jPHSf 0747815384663891403181360803310055645039

おお、やはりあるのか…と思いながら、opensslコマンドでハッシュが間違っていないことを確認しながら、プログラムの出力を眺めていた。

「先輩、けっこうありますね…」

「おう、速いな」

「もう計算できちゃったんですか…」

岡村はシャーペンを机に放って、結果を見にやってきた。

「元メッセージはどう選んだんだい?」

先輩は自分が書いたコードを眺めながらそう言った。

「アルファベット大文字小文字もしくは数字です」

「なるほど…」

先輩は、シャーペンを手にとって何やら書き始めた。

「探したのは5桁のアルファベットもしくは数字のメッセージというわけだ。つまり62の5乗は…」

先輩はシャーペンを手に持ったまま、器用にアドレスバーに数式を入れる。

 62^{5} = 916132832

「およそ9億個ですね」

「ということは、そのうちSHA1が全て数字になるのは…」

岡村は必至に食いついてくる。

「9億に10/16の40乗をかけると…」

\displaystyle 62^{5} \times  \left(\frac{10}{16}\right)^{40} = 6.268 \cdots

「6個です。だいたい」

部長は、後輩の理解が追いついていることに満足そうに頷いた。 それは、自分のプログラムが6桁のメッセージの解を表示しはじめたのと、ほとんど同時だった。

gflyP 9708825594493053358052040804954710052563
OqmSX 5405673447021682949913714302263814023323
pcBsd 0830855821698284247230340812332913765683
jPHSf 0747815384663891403181360803310055645039
IL3rj 3243002591985408609566985352935152776909
MUeWp 8297833274599142233719359578426918109541
JG4Y80 0099530489181060830720140193389029330084
WiMtG0 2254364706744121285147874769100420502311
KMnsR0 4760110574803441139829661498017839123177
...

「つまり今考えているメッセージで5文字で、えっと」

部長がその言葉を遮ってこう続けた。

「アルファベット大文字小文字数字で5文字からなる元メッセージに対して、SHA1ハッシュが全て数字となるのは6つ、およそ期待値通りだね」

岡村も頷いた。

「でも、これに何の意味があるんですかね」

僕は思わず尋ねてしまった。

「意味というのは」

「いや、つまりですよ。SHA1というのは160ビットのバイナリー列って最初に言ってたじゃないですか。この16進数の表現は、なんというか、人間が勝手に4ビットずつ区切って勝手に0からfに割り当てただけじゃないですか」

「なるほど、たしかにそれはそうだね」

「つまり、SHA1ハッシュが数字だろうがアルファベットだろうがSHA1にとっては何の特徴にもならない気がするんです」

「なるほどね」

先輩は頷きながらそう言った。

「でも、それだからこそ、こうやって期待値通り、6個見つかったということにもなるね」

「そこなんですけど…」

岡村が怪訝そうな顔で口を開いた。

「それってつまり、16進数の表現の中で、文字がまんべんなく現れるということを仮定していますよね」

「たしかに」

「本当にそうなんですかね」

先輩はしばらく考えていたが、急に笑顔になって、こう言った。

SHA1の文字が本当に一様に分布するか、計算してみよう!」

その言葉と同時に、下校時刻を告げるチャイムが鳴った。

SHA1ハッシュの文字分布

家に帰ってから、改めて全て数字となるSHA1ハッシュの計算を再開した。 数字とアルファベット6文字の元メッセージは 62^{6} = 56800235584個あり、その中でSHA1が全て数字となるものは397個あった。 期待値は388.64個なので、少し多めに見つかっていることがわかった。 7文字の中でも計算しようとしたが、時間がかかってプログラムが終了しなかった (後日結果を見てみたら、24061個見つかった。期待値は24095.86個、その誤差は0.15%未満となった)。

夕食後、SHA1ハッシュの文字分布を集計するプログラムを組んだ。 元メッセージは先程の計算と同じように、アルファベット大文字小文字もしくは数字で選んだ。 元メッセージの長さを軸に集計すると、次のようになった。

|X| 0 1 2 3 4 5
0 5 (12.500%) 133 (5.363%) 9551 (6.212%) 595152 (6.243%) 36950623 (6.252%) 2290283302 (6.250%)
1 1 (2.500%) 157 (6.331%) 9648 (6.275%) 595439 (6.246%) 36949237 (6.251%) 2290285498 (6.250%)
2 1 (2.500%) 139 (5.605%) 9539 (6.204%) 596209 (6.254%) 36946427 (6.251%) 2290302477 (6.250%)
3 3 (7.500%) 140 (5.645%) 9598 (6.242%) 596671 (6.259%) 36949064 (6.251%) 2290259456 (6.250%)
4 1 (2.500%) 164 (6.613%) 9727 (6.326%) 596720 (6.259%) 36943267 (6.250%) 2290339043 (6.250%)
5 4 (10.000%) 143 (5.766%) 9586 (6.234%) 595214 (6.244%) 36942983 (6.250%) 2290318196 (6.250%)
6 2 (5.000%) 175 (7.056%) 9673 (6.291%) 595193 (6.243%) 36931008 (6.248%) 2290360368 (6.250%)
7 1 (2.500%) 163 (6.573%) 9558 (6.216%) 595012 (6.242%) 36937662 (6.249%) 2290455010 (6.250%)
8 2 (5.000%) 149 (6.008%) 9691 (6.303%) 595441 (6.246%) 36950679 (6.252%) 2290411313 (6.250%)
9 4 (10.000%) 172 (6.935%) 9658 (6.281%) 596341 (6.255%) 36943415 (6.250%) 2290385579 (6.250%)
a 3 (7.500%) 168 (6.774%) 9797 (6.372%) 596924 (6.262%) 36936091 (6.249%) 2290363615 (6.250%)
b 3 (7.500%) 131 (5.282%) 9611 (6.251%) 595164 (6.243%) 36942624 (6.250%) 2290301647 (6.250%)
c 0 (0.000%) 186 (7.500%) 9417 (6.124%) 596098 (6.253%) 36928976 (6.248%) 2290281914 (6.250%)
d 3 (7.500%) 176 (7.097%) 9571 (6.225%) 596065 (6.253%) 36936884 (6.249%) 2290307306 (6.250%)
e 4 (10.000%) 153 (6.169%) 9567 (6.222%) 595888 (6.251%) 36930409 (6.248%) 2290299939 (6.250%)
f 3 (7.500%) 131 (5.282%) 9568 (6.223%) 595589 (6.248%) 36934091 (6.249%) 2290358617 (6.250%)

右に行くほど、つまり沢山のSHA1ハッシュを調べるほど、全ての文字が均等に現れることがわかる。 ある特定の文字だけ沢山出現したり、少なかったりすることはなさそうだ。 この結果に満足して、僕は就寝についた。

SHA1SHA1がだいたい数字

「おお、たしかに収束しているようだね」

昨日の計算結果を見せると、先輩は満足そうにそう言った。

「100を16で割って6.25%ですね」

岡村も頷いている。

「まあ証明したわけじゃないが…」

先輩は軽く咳をしてから頭を掻いた。

SHA1の文字は一様に出現することを仮定して、いろいろな期待値をもとめてみよう」

そう言いながら、先輩はテキストファイルを順番に開きはじめた。

LvwEJe1 6051096335827944381165360280845795286423 111613f48230408056a71706b4174442640b6489
sG9wqMH 9788371029031493501751484666965269206614 e6279615888204167759744340b6c13236273b37
AIJsWgU 4833765415800580402215357543416053382134 823885871864d85103339b7110420321b64b7901
HFrIKpt 1515438875168251864313109613925545684403 820316645881e3020f35e48662206491972878c8
wc9U6dv 5898389906639826050665528530112945836398 5966347988220353b91383721534804a3a278c21

多くの数字が並んでいるが、一番右のSHA1には少しアルファベットが含まれているようだ。

「これはなんですか」

思わず僕は疑問を口にした。

「これは、SHA1ハッシュが全て数字となり、そしてそれをさらにSHA1ハッシュを取った時に、アルファベットが4文字以下となるものだ」

岡村は少し混乱した様子で紙を手に取った。

「つまり…まずは全てが数字で… さらにSHA1ハッシュを取ったら40文字の中で4文字以下がアルファベットとなるので、こうなりますね」

\displaystyle \left(\frac{10}{16}\right)^{40} \sum_{k=0}^{4} {40 \choose k} \left(\frac{6}{16}\right)^{k} \left(\frac{10}{16}\right)^{40-k} = 6.687 \times 10^{-13}

先輩は空中で指を振りながら、計算が間違っていないことを確かめながら口を開いた。

「そうそう、元メッセージは昨日と同様、アルファベットもしくは数字の7文字以下で探索したものだから… その数を掛けて…」

\displaystyle \cdots \times \sum_{i=0}^{7} {62}^{i} = 2.394\cdots

「期待値はこれくらいになる」

「あ、少し低くないですか」

岡村がすぐに指摘した。

「期待値が2.4で、見つかったのが5個ですか」

「まあこういうこともあるさ。次のファイルは」

HQC8su9 03003251839018941492807233288828427481a2 3395664741163489868183a7392270978a505005
VByKdTE 027985658547959903b826850439976465234220 1a3268963b445974667880579063493748834739

SHA1ハッシュと、そのさらにSHA1ハッシュをあわせた80文字の中で、アルファベットが3文字しか含まれないものだ。2つしか見つからなかった」

\displaystyle \sum_{k=0}^{3} {80 \choose k} \left(\frac{6}{16}\right)^{k} \left(\frac{10}{16}\right)^{80-k} \sum_{i=0}^{7} {62}^{i} = 3.173\cdots

「今度は期待値が少し高いんですね」

僕はファイルのSHA1を見つめ、目を凝らしてどこにアルファベットがあるのかを探しながらそう言った。

「でも、昨日考えていた、全てが数字になるものは期待値通りでしたよね」

「だいたいね」

「要するに、3個とか4個とかの時はあまり合わないけど」

「沢山見つかるほど、見つかった数と期待値の誤差が小さくなるということですね」

「それはまあ、そうだろうね」

岡村は自分のディレクトリに移動してテキストファイルを開いた。

「ところで僕も探索してみました」

ZDyHlIM 5954449490599416666154915409164008649984

そのファイルには一行しか書かれていなかった。

「これは?」

「全て数字なんですけど、7種類の数字しか含まれていないんです」

「なるほど…」

先輩も身を乗り出して後輩が見つけたSHA1ハッシュを眺めている。

「5と9と4と… 0と6、1、それから8で… 確かに7つの数字みたいだね」

「そうなんです。アルファベットと数字7文字以下の文字列のSHA1の中ではこの1つしか見つかりませんでした」

「貴重だね」

僕も感心して思わず軽く口笛を吹いた。

文字連続

先輩は、また計算結果が書かれたファイルを開いてみせた。

dGtWcS d6141925ca72293fba0b5f43000000000000f70b
9J2DJv8 a9fc511111111111125d3d87910d68ddfe350f24
QDi79qF c80b072b3f2c2b6b34cb404cfc2555555555555a
cE2sXYZ d59938cf4afe4effffffffffff6f8181ef568bc1
y1m5aTz b60a1e6666666666665f09a5d8a0c05eb382dca0

「これは、同じ文字が12個連続するところがあるSHA1ハッシュだ」

「おおお」

「それから、13個連続するのも1つ見つけることができた」

rcb0lQv 3333333333333455f30f9a9f761fb8e94b906a0f

「なんかすごい」

「つまり、12個以上連続するものがいま6個あるのだけど…」

そう言いながら先輩はシャーペンを持って期待値を計算しはじめた。

「連続している文字が16通りで、その位置が40-12+1通りだから…」

\displaystyle 16 \cdot (40 - 12 + 1) \left(\frac{1}{16}\right)^{12} \sum_{i=0}^{7} {62}^{i} = 5.900 \cdots

「だいたい期待値通りですね」

岡村は期待値の式を眺めながら数式の意味を追っている。

「他にもこんなものも見つかった」

先輩はそう言いながらファイルを開いた。

w5PPbh5 222222222220101b01d906e32e61373b45265361
bzajR5G 555555555558a91de2fb2fa98af5048bfd887c65
daycahL 2222222222281376525c8dbab5a18e3ca933d5c3
Q4dDYWa fffffffffffa03a907d9650eb91bc99642a86199
2o0XCwe 888888888886d55909a93a870fe44fd91aea5392

「先頭11文字が同じSHA1ハッシュだ」

「おもしろいですね」

GitHubのリンクで、こんなの見つけたらスクショを取りたくなりますね」

「そうそう、こんなSHA1はおもしろい、ただそれだけでいいと思うんだよね」

さらに珍しいSHA1を目指して

部室に夕日が差し込んでいる。まもなく下校時刻になる。

「色々な性質のSHA1ハッシュが集まりましたね」

岡村は感慨深くため息を付いた。

「そうだね。まず考えたのが全て数字、二回SHA1を取ったもの、それから岡村くんが見つけてくれた、7つの数字しか含まれていないもの、先輩が計算した連続文字…」

「いろいろあるね」

先輩はしばらく夕日を眺めてから、口を開いた。

「全て同じ文字となる確率はどれくらいかな」

岡村はアドレスバーに式を打ち込んで答えた。

\displaystyle 16 \cdot \left(\frac{1}{16}\right)^{40} = 1.095\cdots \times 10^{-47}

「10の-47乗ですね。だいたい」

「なるほど」

先輩は頷きながら暗算してこう続けた。

SHA1が全て0となる確率は、それを16で割ると、6、いや7かける10の-49乗くらいかな」

「そうですね」

岡村は式を調整しながら答えた。

「じゃあ、いくつSHA1を探したらどのくらいの確率で全て同じ文字のものを見つけられるかな」

先輩は少し意地悪そうにこう言った。

「えっと、さっきのを1から引いて… あれ」

どうやら検索エンジンの誤差で計算できないようだ。僕はすかさずWolframAlphaを開いて計算してみせた。

\displaystyle \left(1 - 16 \cdot \left(\frac{1}{16}\right)^{40}\right)^{10^{46}} = 0.8963\cdots

「10の46乗個探すと、10%の確率で見つかる計算になります」

「1秒に10の7乗個のSHA1を計算できるパソコンがあるとしよう」

岡村はすぐに計算して結果を答えた。

「およそ3かける10の31乗年かかりますね」

「宇宙ができてから確か138億年だよね」

僕がそう言うと、すぐに岡村は計算結果を割って答えた。

「宇宙が10の21乗個できちゃいますね」

「そりゃあ、大変だなぁ」

先輩が夕日を眺めながらそう言うと、下校のチャイムが鳴った。どのくらいの性質のSHA1なら現実的な時間で見つけられるか、そんなことを議論しながら、僕らは帰路に着いた。

git grepで仕事してる

私はコードを書く時に頻繁にgit grepを使っていて、一日に何回くらいgit grepを使っているのか気になったのでログを取ってみました。

2016 10/24 月: 61
2016 10/25 火: 36
2016 10/26 水: 19
2016 10/27 木: 80
2016 10/28 金: 51
2016 10/31 月: 96
2016 11/ 1 火: 47
2016 11/ 2 水: 53
2016 11/ 4 金: 84
2016 11/ 7 月: 56
2016 11/ 8 火: 33
2016 11/ 9 水: 19
2016 11/10 木: 71

これは私が会社のPCでエディタの中でgit grepしたログを集計したものです。 結構ばらつきはありますが、40〜50回くらい、多い日で100回近くgit grepしているということが分かりました。 仕事の時間を割って平均したら大体5〜10分に一回はgit grepしていることになります。 もちろんコンスタントに使っているわけではなく、コードを書く時は増えますしミーティングがあれば減ります。 ですから、会議の多い日や設計段階を考えている日などは少なくなります。

私はVimScala・TypeScript・Goなどを書いています。 例えば、サーバーサイドを触る時にはコードとテンプレートを行き来するためにgit grepしますし、フロントエンドのコードも大体サーバーサイドと似たような変数名でモデリングしているため、git grepで関連ファイルを開いています。 JSONのフィールド名でもgit grepしますし、サイトのこの部分を変更したいという時は、クラス名を拾ってgit grepしてhtmlファイルを開きます。 ファクタリングや変数名関数名の変更も、まず最初にgit grepし、全て変更してからコンパイルします (手元で継続的にコンパイルするのはつらい)。 昔はcexprを使ってquickfixに流していましたが、今はlexprでlocation listに流してファイルを開いています。

他の人のコードのレビューする時は、動作確認したあとに、変更箇所の関数が使われている場所を必ず見るようにします。 関数に限らず、オブジェクト名でも、CSSのクラス名でも、サイトのパスの一部でも、なんでもgit grepします。

ある一つのプロジェクトのコードを長いこと触っていると、大体のコードの見た目が頭に入ってしまいいます。 DBでgroup byして引くときのORMのコードパターンはこんな感じなので…みたいな曖昧な記憶でも、git grepで目的のコードを開けたりします。 他のケースとして、エラーメッセージが出た時には、そのメッセージでgrepしてメッセージIDを見つけて、さらにそのメッセージIDでgrepするという二段git grepが発動します。

外部ライブラリも大体手元に落としてきてgrepするようにしています。 自分たちのコードからctagsで外部ライブラリに飛び、さらにそのリポジトリの中でgit grepするということがよくあります。

とにかくgit grepに頼っているので、たぶん自分はgit grepで仕事しているんだと思います。

コードを書いたり読んだりすることばかりが仕事ではないので、単純には仕事量の尺度にはなりませんが、しばらくログを取ってみて集計してみるとおもしろいことが分かったりするかもしれません。ぜひログを仕込んで回数を調べてみて下さい。あなたは一日に何回くらいgit grepしていますか?