Continuation based C の
GCC 4.6による実装

Presenter Notes

研究目的

plop

  • 状態遷移記述をベースとしたより細かい単位でのプログラミングを実現する
    • 組込みソフト、Real-time処理、通信プロトコル記述、どれも状態遷移ベース
    • 現存する記述言語は状態遷移の記述に向いていない
    • スタックが状態を隠蔽するため、分割しにくい、検証が難しい
    • 当研究室ではコードセグメント単位で記述するプログラミング言語Continuation based C (以下CbC)という言語を提案している。
    • CbC のコンパイラは Micro-C 版 と GCC ベース(以下 CbC-GCC)のコンパイラが開発されている。
    • しかし, CbC-GCC はいくつかのバグがあり機能の修正の余地があった。
    • また、GCC の最新の機能を使用するためにも CbC-GCC は GCC のアップデートに合わせていく必要がある。

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

    Presenter Notes

    Continuation based C

    コードセグメント単位での記述と継続を基本としたプログラミング言語

  • コーセグメント:CbC におけるプログラムの基本単位
    • C の関数よりも細かな単位
    • 構文は C と同じだが、C から関数コールとループ制御が取り除かれた形
    • コードセグメントの末尾処理で別のコードセグメントへ継続(goto)することで CbC のプログラムは続いていく。
    __code cs_a(int num) {
       :
       :
       goto cs_b();
    }
          

    Presenter Notes

    Continuation based C

    継続:現在の処理を実行していく為の情報

    Cの継続

    CbCの継続(軽量継続)

    • 続く命令のアドレス
    • 命令に必要なデータ
    • スタックに積まれている値(環境)
    • Cの継続から環境を除外
    • 続く命令とその命令に必要なデータのみ

    Presenter Notes

    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 命令でコードセグメントの処理が遷移している
  • Presenter Notes

    Gnu Compiler Collection (GCC)

  • GCC: オープンソースのコンパイラ群
  • ここで扱う GCC はソースコードをアセンブラに変換する『cc1』のことを指す。
  • GCC がソースコードを読み込みアセンブラ言語を出力するまでの流れは以下の図のようになる。
  • GCC のアセンブラ言語出力までの流れ
  • ソースコードはアセンブラに変換される間に 4 つのデータ構造に変換される。
  • この中でも CbC の実装では Parser と RTL の生成部分に手が加えられている。
  • Presenter Notes

    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;
      }

    Presenter Notes

    CbC の実装: Tail Call Elimination

    Tail Call Elimination の条件

    • 関数を呼び出す側(caller)と呼び出される側(callee)の戻値の型が一致している。
    • 関数呼び出しがリターン直前に行われている。
    • 呼び出される関数(callee)の引数に用いられるスタックサイズが呼出元(caller)のそれより少ない。
    • 引数の並びのコピーに上書きがない。

    条件回避の為の実装

    • コードセグメントの型はvoid型で統一する。
    • gotoの直後に自動で return を置く。
    • スタックサイズは固定にする。
    • 引数は一旦、一時変数にコピーする。

    Presenter Notes

    CbC の実装: Tail Call Elimination

    Tail Call Elimination の条件をチェックする関数

    • 今までの実装では Tail Call Elimination の条件をクリアする為に専用の関数を用意していた。
    • この専用関数は元々ある GCC コードを元に作成している為, アップデートに合わせて修正していく 必要があった。

    実装の修正とその利点

    • しかし, 今回の実装でその関数を無くし, Tail Call Elimination のフラグを強制的に立たせる実装に変更。
    • 専用関数がなくなったことで、今後より楽なアップデートを行なっていくことができるようになった。
    • また、コードセグメントへの継続が jmp ではなく call 命令で行われるバグがあったが修正できた。

    Presenter Notes

    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倍以上早い
  • Presenter Notes

    GCC-4.6 の最適化

    • GCC-4.6 では最適化の 1つ『インライン展開』が強化されている。

    インライン展開

    • 関数の処理をそのまま関数呼び出し部に展開することで call を省略できる最適化

    インライン展開の例

    void func(){
      A;
      B;
    }
    
    int main() {
      func();
      func();
    }
    
    int main() {
      A;
      B;
      A;
      B;
    }
    
  • func の呼び出しがなくなっている。
  • Presenter Notes

    最適化の比較

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

    最適化なし GCC-4.5の最適化(-O2) GCC-4.6の最適化(-O2)
    • 最適化無しに比べると GCC-4.5、 GGC-4.6 共にコードセグメントの数が減っている。
    • GCC-4.5 でもインライン展開はされているが、GCC-4.6 はより良い最適化がかけられている。

    Presenter Notes

    最新版のアップデートに合わせる有用性

    • 今回の『インライン展開』のように GCC の最適化は日々改良されている。
    • また、既存の最適化の改良だけでなく新たな最適化の追加等も行われていく。
    • それらの恩恵を受ける為にも GCC のアップデートに合わせていく事は今後も重要である。

    Presenter Notes

    まとめ

    • 今回 CbC-GCC を GCC-4.6 へとアップデートを行った。
    • アップデートにより、よりよいコードを生成する CbC のコンパイラを用意することができた。
    • また、最適化の強制付与やといった実装の修正も行えた。
    • 細かな実装を除けば, CbC-GCC は今後 GCC のアップデートに合わせていくだけとなる。

    今後の課題

    • LLVM ベースの CbC コンパイラの開発
    • google Go 言語への実装の検討

    Presenter Notes

    jmp と call

    インライン展開無しの conv1 プログラム実行結果

    Presenter Notes

    構文の追加

    リカーシブタイプの宣言に使う"__rectype"

    • 関数宣言時、以下のように引数に自分自身を指す型を入れたい。
      __code func(int, func*);
      
    • 上記の宣言ではエラーがでる。その為、以下のような宣言になる。
      __code func(int, __code (*)());
      
    • しかし、これでは正しい情報をコンパイラに渡せていない。関数ポインタの引数に型情報が入っていないからである。
      __code func(int, __code ()(int, __code()(int, __code(*)(int, ...))))
      
    • だが、正しい情報を渡そうとすると上記のように再帰してしまい、宣言できない。
    • そこで __rectype という構文を追加して宣言中の関数自身を指すようにした。
      __code func(int, rectype*);
      

    Presenter Notes

    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);
      }
      

    • 関数ポインタへの継続はインライン展開されない。

    Presenter Notes

    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);
      }
      

    • 今回この環境付き継続をスレッドセーフの実装へと修正した。

    Presenter Notes

    CbC 引数渡し

  • CbC では引数渡しにできるだけレジスタを用いるようにしている.
  • fastcall属性有・無の実行速度
    fastcall無し
    fastcall有り

    Presenter Notes

    引数の並びに上書きコピー

  • 以下の呼び出しを行うと、スタックの書き換えがおこる
  • void funcA(int a, int b) {
      funcB(b, a);
    }
    

    Presenter Notes

    最適化の比較

    各コンパイラにより生成されたコードの速度比較
    x86/Linux x86/OS X (10.7)

    Presenter Notes

    最適化の比較

    それぞれの最適化により吐かれたアセンブラコード
    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
          
  • 関数を展開してその場で計算する『インライン展開』がより強力になっているのが確認できる
  • ---

    Presenter Notes