Continuation based C の
GCC 4.6による実装 ========= --- 研究目的 --------- .notes:
  • 状態遷移記述をベースとしたより細かい単位でのプログラミングを実現する
  • - 本研究室ではコードセグメント単位で記述するプログラミング言語Continuation based C (以下CbC)という言語を提案している。 - CbC のコンパイラは Micro-C 版 と GCC 版(以下 CbC-GCC) が開発されている。 - しかし, CbC-GCC はいくつかのバグがあり機能に修正の余地があった。 - また、GCC の最新の機能を利用するためにも CbC-GCC は GCC のアップデートに合わせていく必要がある。

    本研究では CbC-GCC のアップデートを行い、より良いコードを生成する CbC の処理系を開発した。

    --- Continuation based C ======== コードセグメント単位での記述と継続を基本としたプログラミング言語 ---------
  • コーセグメント:CbC におけるプログラムの基本単位
  • __code cs_a(int num) {
       :
       :
       goto cs_b();
    }
          
    --- Continuation based C ======== 継続:現在の処理を実行していく為の情報 ---------

    Cの継続

    CbCの継続(軽量継続)

    • 続く命令のアドレス
    • 命令に必要なデータ
    • スタックに積まれている値(環境)
    • Cの継続から環境を除外
    • 続く命令とその命令に必要なデータのみ
    --- Continuation based C ========
    階乗を求める CbC のプログラム
    \_\_code print_factorial(int prod)
    {
      printf("factorial = %d\n",prod);
      exit(0);
    }
    \_\_code factorial0(int prod, int x)
    {
      if ( x >= 1) {
        goto factorial0(prod\*x, x-1);
      }else{
        goto print_factorial(prod);
      }
    }
    \_\_code factorial(int x)
    {
      goto factorial0(1, x);
    }
    int main(int argc, char \*\*argv)
    {
      int i;
      i = atoi(argv[1]);
      goto factorial(i);
    }
    
  • __code によるコードセグメントの宣言
  • 引数付きの goto でコードセグメントを呼び出すことができる
  • 内部では call ではなく jmp 命令でコードセグメントの処理が遷移している
  • --- Gnu Compiler Collection (GCC) ========
  • GCC: オープンソースのコンパイラ群
  • ここで扱う GCC はソースコードをアセンブラに変換する『cc1』のことを指す。
  • GCC がソースコードを読み込みアセンブラ言語を出力するまでの流れは以下の図のようになる。
  • GCC のアセンブラ言語出力までの流れ
  • ソースコードはアセンブラに変換される間に 4 つのデータ構造に変換される。
  • この中でも CbC の実装では Parser と RTL の生成部分に手が加えられている。
  • --- CbC の実装 : 軽量継続 ========
  • CbC-GCC の軽量継続は最適化の1つ, Tail Call Elimination(末尾除去)により実装されている.
  • Tail Call Elimination ---------
  • 関数呼び出しを call ではなく jmp 命令で行う最適化
  • 例えば、以下の場合関数 g は jmp 命令で関数 f へと処理が移る
  •   void f(int a, int b) {
        printf("f: a=%d b=%d\n",a,b);
        return ;
      }
    
      void g(int a, int b){
        printf("g: a=%d b=%d\n",a,b);
        f(a,b);
        return;
      }
    
      int main() {
        g(3,4);
        return 0;
      }
    --- CbC の実装: 末尾除去の強制 ======== 末尾除去の条件をチェックする関数 --------- - 関数が末尾除去にかかる為には幾つか条件があり、その条件はある関数によってチェックされる。 - その関数を元に、『コードセグメントは末尾除去』にかける専用の関数を用意していた。 - しかしこの方法は、元になった関数が修正に合わせて専用の関数も修正を行う必要があった。 実装の修正とその利点 --------- - しかし, 今回の実装でその関数を無くし, 末尾除去のフラグを強制的に立たせる実装に変更。 - 専用関数がなくなったことで、今後より楽なアップデートを行なっていくことができるようになった。 - また、コードセグメントへの継続が jmp ではなく call 命令で行われるバグがあったが修正できた。 --- GCC-4.5, GCC-4.6 の性能比較 ========
  • 本研究では GCC-4.5 から GCC-4.6 へのアップデートを行った。
  • この 2 つのバージョンを用いて生成したプログラムの速度比較を行った。
  • conv1: 加算と継続を交互に繰り返すプログラム
  • 各コンパイラにより生成されたプログラムの速度比較
    x86/Linux x86/OS X (10.7)
  • Mac の GCC-4.5 では動かなかった 32bit のプログラムが GCC-4.6 では問題なく動いている。
  • 引数 2、3 は手動で最適化をかけている。
  • 引数 1 の結果では 32bit, 64bit 共に GCC-4.6 の方が 1.5倍以上早い
  • --- GCC-4.6 の最適化 ======== - GCC-4.6 では最適化の 1つ『インライン展開』が強化されている。 インライン展開 --------- - 関数の処理をそのまま関数呼び出し部に展開することで call を省略できる最適化

    インライン展開の例

    void func_b(){
      A;
      B;
    }
    
    void func_a() {
      func_b();
      func_b();
    }
    
    void func_a() {
      A;
      B;
      A;
      B;
    }
    
  • func_b の呼び出しがなくなっている。
  • ---

    最適化の比較

    それぞれの最適化にかかった conv1プログラムの挙動(引数 1)

    最適化なし GCC-4.5の最適化(-O2) GCC-4.6の最適化(-O2)
    - 最適化無しに比べると GCC-4.5、 GGC-4.6 共にコードセグメントの数が減っている。 - GCC-4.5 でもインライン展開はされているが、GCC-4.6 はより良い最適化がかけられている。 --- 最新バージョンに合わせる有用性 ======== - 今回の『インライン展開』のように GCC の最適化は日々改良されている。 - また、既存の最適化の改良だけでなく新たな最適化の追加等も行われていく。 - それらの恩恵を受ける為にも GCC のアップデートに合わせていく事は今後も重要である。 --- まとめ ======== - 今回 CbC-GCC を GCC-4.6 へとアップデートを行った。 - アップデートにより、よりよいコードを生成する CbC のコンパイラを用意することができた。 - また、最適化の強制付与といった実装の修正も行えた。 - 細かな実装を除けば, CbC-GCC は今後 GCC のアップデートに合わせていくだけとなる。 今後の課題 --------- - LLVM ベースの CbC コンパイラの開発 - google Go 言語への実装の検討 --- CbC ========
  • 状態遷移記述をベースとしたより細かい単位でのプログラミングを実現する
  • 組込みソフト、Real-time処理、通信プロトコル記述、どれも状態遷移ベース
  • 現存する記述言語は状態遷移の記述に向いていない
  • スタックが状態を隠蔽するため、分割しにくい、検証が難しい
  • 以上の問題を解決する言語として CbC は提案されている。
  • 具体的な CbC の利用予定 --------- - モデル検査 - Cerium の実装 --- Tail Call Elimination ======== 関数が Tail Call Elimination にかかる条件 --------- 条件回避の為の CbC の実装内容 --------- --- jmp と call ========
    インライン展開無しの conv1 プログラム実行結果
    --- conv1 プログラム ========
  • 性能評価で用いた conv1 プログラムの C 版
  • f0(int i) {
        int k,j;
        k = 3+i;
        j = g0(i+3);
        return k+4+j;
    }
    g0(int i) {
        return h0(i+4)+i;
    }
    h0(int i) {
        return i+4;
    }
  • 性能評価はこのプログラムを CbC へと書き換えて行なっている。
  • --- CbC-GCC のアップデート手法 ========
    1. GCC のソースを入れるリポジトリを用意する。
    2. GCC のリポジトリの中身を全て消し、新しい GCC を入れて新しいファイルは追加、消えたファイルは削除する。
    3. コミット
    CbC-GCC のリポジトリ ---------
    1. GCC のソースから pull
    2. merge を行う
    3. 衝突のあったファイルを手動でマージする
    4. コミット
    --- 構文の追加 ======== リカーシブタイプの宣言に使う"__rectype" --------- - 関数宣言時、以下のように引数に自分自身を指す型を入れたい。
    \_\_code func(int, func*);
    
    - 上記の宣言ではエラーがでる。その為、以下のような宣言になる。
    
    \_\_code func(int, \_\_code (*)());
    
    - しかし、これでは正しい情報をコンパイラに渡せていない。関数ポインタの引数に型情報が入っていないからである。
    \_\_code func(int, \_\_code (*)(int, \_\_code(*)(int, \_\_code(*)(int, ...))))
    
    - だが、正しい情報を渡そうとすると上記のように再帰してしまい、宣言できない。 - そこで __rectype という構文を追加して宣言中の関数自身を指すようにした。
    \_\_code func(int, rectype*);
    
    --- conv1 プログラム ======== - conv1 プログラムでは計算と継続を交互に繰り返し行なう。 - しかし状態のいくつかへは関数ポインタとして保存させておき継続を行う。
    \_\_code g(int i,stack sp) { // Caller
        struct f_g0_interface *c = 
            (struct f_g0_interface *)(sp -= sizeof(struct f_g0_interface));
        c->ret = g_h1;
        c->i_ = i;
        goto h(i+3,sp);
    }
    \_\_code h(int i,stack sp) {
        struct f_g0_interface *c = (struct f_g0_interface *)sp;
        goto (c->ret)(i+4,sp);
    }
    
    - 関数ポインタへの継続はインライン展開されない。 --- CbC の実装: 環境付き継続 ======== - 環境付き継続: C との互換を取るための機能。継続を行った C の関数に戻ることができる。 - _CbC_return、 _CbC_environment キーワードを使うことで使える。 - 以下の使い方の場合、戻値 1 を返す。
    __code c1(__code ret(int,void *),void *env) {
        goto ret(1,env);
    }
    int main() {
        goto c1(__return, __environment);
    }
    
    - 今回この環境付き継続をスレッドセーフの実装へと修正した。 --- CbC 引数渡し ========
  • CbC では引数渡しにできるだけレジスタを用いるようにしている.
  • fastcall属性有・無の実行速度
    fastcall無し
    fastcall有り
    --- 引数の並びに上書きコピー ========
  • 以下の呼び出しを行うと、スタックの書き換えがおこる
  • void funcA(int a, int b) {
      funcB(b, a);
    }
    
    --- 最適化の比較 ========
    各コンパイラにより生成されたコードの速度比較
    x86/Linux x86/OS X (10.7)
    --- 最適化の比較 ========
    それぞれの最適化により吐かれたアセンブラコード
    CbC-GCC-4.5 CbC-GCC-4.6
    main:
    	call    f
    	:
    	jmp     f_g0
    	:
    	movq    $f_g1, -24(%rdx)
    	addl    $10, %edi
    	movq    $g_h1, -48(%rdx)
    	jmp     g_h1
    	:
    	movq    24(%rsi), %rdx
    	movq    %rax, %rsi
    	:
    	jmp     *%rdx
    	:
    	movq    24(%rsi), %rdx
    	:
    	jmp     *%rdx
          
    main:
    	movq    $f_g1, main_stack+2000(%rip)
    	:
    	call    g_h1
    	:
    	movq    24(%rax), %rdx
    	:
    	jmp     *%rdx
    	:
    	movq    24(%rax), %rdx
    	:
    	jmp     *%rdx
          
  • 関数を展開してその場で計算する『インライン展開』がより強力になっているのが確認できる
  • ---