Skip to content

Instantly share code, notes, and snippets.

@kekyo
Last active April 25, 2024 03:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kekyo/d3bec6e1427bc8c0d459f7fcb7a1fcac to your computer and use it in GitHub Desktop.
Save kekyo/d3bec6e1427bc8c0d459f7fcb7a1fcac to your computer and use it in GitHub Desktop.
chibicc-cil残件
Repository Url
YouTube https://bit.ly/3XbqPSQ
chibicc-cil https://github.com/kekyo/chibicc-cil (まだ非公開)
chibias-cil https://github.com/kekyo/chibias-cil

ldargaに対応して、プロローグコードでローカル変数にコピーするのをやめる

解決済み: 80669fb0377

  • Objにkindを持たせて、引数とローカル変数を区別可能にする。
  • gen_adder()ldlocaしているところを、このタイプを見て切り替える。
  • プロローグコードのコピーコードを削除する。
  • ほか、ローカル変数インデックスの整合性とか確認する。

関数パラメータに配列型が存在すると、使われないメモリをlocallocで生成してしまう

解決済み: b1a1b72ecc

  • 値型配列を導入して解決。

解説: https://www.youtube.com/watch?v=s0Se886xllU

  • 上のトピックに絡む話。
  • 動画コメント欄参照。
  • fn->paramsfn->localsを分けて管理する?
    • 分けて管理すれば、関数プロローグコードのemit時に、余計なコードを生成しなくても良くなる。
  • 分けた場合、パラメータを参照するときはldlocaではなくldargaを使う必要がある。

ND_IFのthenとelseステートメントで、実行されない不正なCILコードが出力されるのを修正する

解決済み: 405141080ed

  • MaxStackSizeの問題を発見するに至ったバグ。
  • CIL instruction stream上には、実行されないCILコードが存在したとしても、それが評価スタックの静的実行フロー解析結果に影響を与えてはならない。
  • pop, br LABELみたいなコードが出てしまう。popが評価スタックを消費するので、不正な計算をしてしまう。
  • ステートメントとして評価する箇所がおかしい?

x86 (32ビット) 環境でしか動かない問題を修正する

解決済み: 27e509038426cb

  • ポインタサイズを4バイトと仮定している。
  • sizeof opcodeを使って実行時に計算する必要があるが、chibicc内で計算してしまっている。
  • Type.size がintの直値になっているのを、Node*にして、計算するコードを出力するように変更すればいけそう?
  • sizeof出来るノード(ND_SIZEOF?)が必要? 3e55cafef でやってそう?
  • 79f5de21eb706ea "Add constant expression" で追加されるeval関数のように、再帰的にノードを探索することで、事前に直値の計算ができる(reducer)。 結果をそのままgen_expr()にかければ、直値ならND_NUMで直値が、そうでなければ式を実行するOpCode群が出力される。

MasStackSizeの問題

解決済み: MaxStackSizeをchibiccないしchibias内部で計算しない。

  • cecilでMaxStackSizeを指定しても無視されて、cecil内部で計算された値が出力されてしまう。
  • PR投げた: jbevain/cecil#892
  • ディスカッションの結果、MaxStackSizeは常時必ず静的解析によって決定できる(cecilの内部で)事がわかった。
  • 実行時に評価スタックの最大値が変化するような、以下のようなコードをCILとして作ることはできるが、CLRがこれを実行できることをEMCA仕様上は担保しない(静的解析で決定できないため)。
int32 foo(int32 count)
{
  .locals ([0] int32 i)

  ldarg.0   ; i = count
  stloc.0

L1:
  ldc.i4.1  ; [increase ev stack]

  ldloc.0   ; i--
  ldc.i4.1
  sub
  stloc.0

  ldloc.0   ; if (i != 0) goto L1
  ldc.i4.0
  ceq
  brfalse L1

  ; (snip: decrease ev stack by count)

  ret
}
  • chibias内でこれを検出してwarningを出しても良いかもしれない(削除した実行フロー解析をいじれば出来そう)

末尾再帰最適化に対応する

  • chibiccの実装のおかげで、それほど大変でもない気がしてきた(自分で展開するのではなく、.tailcallを使用)
  • せっかく.tailcall出来るので... C#でもできないし。
  • やるならオプションで有効化(.tailcallされるとデバッガがスタックフレームをたどれなくなるのでデバッグしやすさに影響がある)

Type構造体に型名をキャッシュする

  • codegenのto_typename()で型名を毎回生成している。
  • ポインタをTypeにキャッシュできる。
  • 対応済み。

Node構造体に簡約した結果をキャッシュする

  • 何度もreduceした場合に高速化される。
  • いや、is_reducedフラグを用意すればいい...
  • 対応済み。

ローカル変数が配列型の場合にSystem.Arrayを使用するように選択できるようにする

解決済み: b1a1b72ecc

  • 値型配列を導入して解決。

解説: https://www.youtube.com/watch?v=s0Se886xllU

  • 効率重視とchibiccの実装に寄せるため、ローカル変数をポインタ (int*) として配置するようにした。
  • 当初実装(ビデオ中で"V1"と呼んでいた)では int[] としていたけど、ヒープアロケーションを避けたかった。
  • しかし、int* だと、デバッガで配列の中身を普通に参照できないので、それはそれで辛そう。
  • やるならオプションで有効化するのが良いんじゃないかと思った。
  • 注意深く実装しないと、そのまま翻訳するとネストした配列が文字通りのjaggedになってしまう。ネストした配列こそ多次元配列型を使うと良いかもしれない。

chibiasのラベルfixupをILGeneratorに任せる

  • ILGeneratorの事を失念していて、ラベルのfixup処理を全部自前で書いてしまった。
  • ちゃんと動いているからいいけど、switch OpCodeを実装するときにめんどくさそう...
  • ILGeneratorを使ってラベルを管理するようにしたほうが良いと思う。
  • InlineSwitch対応するときにも、ILGeneratorベースになっていたほうが実装しやすそう。

勘違いっぽい? cecilのILProcessorには、DefineLabel()などは存在しない。

ネイティブDLL呼び出し

extern __declspec(dllimport("foo.so")) void bar(int a, char* b);

みたいなのを定義できると良いのでは?

  • 内部では、P/Invokeのエントリを生成して、実際にDLL呼び出しに解決させる。
  • めんどくさいので、C#のラッパーライブラリを作るので十分だと判断すれば、ボツにする。
  • 方式3パターン案:
    • __declspec(dllimport("foo.so"))のようにライブラリ名書かせる: もっとも実現が容易で、マルチプラットフォーム対応の支障はない。元の属性指定には存在しない構文なのがネック。
    • __declspec(dllimport)や、書かなくても認識できる: コマンドラインに指定されたライブラリファイルを読み取り、自力でシンボル名の存在チェックを行う必要がある。現状、マルチプラットフォームで使えるポータブルなシンボル名解決方法がなさそう。linux系はnmを使えるが、もしかしたら出力フォーマットがプラットフォームで違うかもしれない。又はlibelfを使う。Macやそのほかの対応方法がわからんので、nmに丸投げは移植のしやすさを考えると現実的かもしれない。Windowsはimagehlp.dllを使う?
  • ELFとPEにしか対応できないが、.NET Coreとmonoが動く対象を考えると、まあこれでも良いかとは思う(Macが問題)。
  • ライブラリ名書かせることが出来て、かつ、書かなくてもライブラリを指定すれば自動的に認識できる、と言うのが、互換性においても柔軟性においてもベターだと思う。
  • 資料: libelf: https://atakua.org/old-wp/wp-content/uploads/2015/03/libelf-by-example-20100112.pdf
  • 資料: imagehlp: https://stackoverflow.com/questions/4606628/viewing-export-table-on-an-unmanaged-dll-in-c-sharp
  • 資料: imagehlp: https://stackoverflow.com/questions/4353116/listing-the-exported-functions-of-a-dll

Macはちょっと調べたけど、端的に言って何言ってるかよくわからん感じなので、興味あると手を挙げた人が居れば任せようと思う(.soは.oってことですかね、それにしてはdlopenでロードできるとも言われてて、本当に歴史的事情なのかどうかもわからん...)

JetBrainsから近しいものが出てるけど、シンボルのトラバースは出来ない?

PE

ELF

chibildの方で自動で対応できるように検討中。

__declspecで出来ない事もないけど、結局chibildの方にも手を入れる必要はある。それに-lfooとかやってlibfoo.soが参照されて欲しいよね感ある。 chibicc側からは関数シンボル(コールサイト含む)が提供されれば、ネイティブライブラリにシンボルが存在するかどうかを確認し、発見した場合はコールサイトからDllImportを構築できるはず。

  • chibiccはcallに常にコールサイトを付けるようにする。chibild側は必須ではないが、コールサイトが無い場合はネイティブライブラリとリンクできない(無視するか?エラーにするか?)

enum型をCILで定義させる

解決済み: a0ff71d42c780

  • chibiasは、.enumeratiuonディレクティブをサポートして、出力できるようにする。 0.20.0で実装済み。
  • chibiccは、 c82c8335aa55 の後で、メタデータを出力するように実装する。

Alignmentの対処

解決済み: 7b6121a9f7160

  • 現状では、構造体メンバーにのみ適用し、グローバル・ローカル変数への指定は無視かまたは警告を表示という方向に逃げようと思う。
    • グローバル・ローカル変数に指定したいという欲求は、_mm_load_si128()とかやりたい人ですよね? そもintrin.hはCLRではできない。
  • chibiasでalignment指定できるようにしようと思ったが、再計算が面倒なので、chibicc側でパディングメンバーを生成する方針に変更。

以下、検証:

#include <stdio.h>
#include <stdint.h>

struct x1 {  // 8
  char a;    // 0
  int b;     // 4
};

struct x2 {  // 16
  _Alignas(4) char a;    // 0
  _Alignas(8) int b;     // 8
};

struct x3 {  // 16
  struct x1 a;    // 0
  struct x1 b;    // 8
};

struct x4 {  // 24
  struct x1 a;    // 0
  struct x2 b;    // 8
};

struct x5 {  // 24
  _Alignas(8) struct x1 a;    // 0
  _Alignas(8) struct x2 b;    // 8
};

struct x6 {  // 8
  _Alignas(8) char a;    // 0
  _Alignas(4) int b;     // 4
};

struct x7 {  // 28
  struct x6 a;    // 0
  struct x2 b;    // 12
};

struct x8 {   // 8 / 12
  char *a;    // 0
  int b;      // 4 / 8
};

struct x9 {   // 16
  int a;      // 0
  _Alignas(8) char b;   // 8
  int c;      // 12
};

#define PTR(p) ((uint8_t*)(p))

void main()
{
    struct x1 x1[2];
    struct x2 x2[2];
    struct x3 x3[2];
    struct x4 x4[2];
    struct x5 x5[2];
    struct x6 x6[2];
    struct x7 x7[2];
    struct x8 x8[2];
    struct x9 x9[2];

    printf("x1=%ld, %ld\n", PTR(&x1[1].a) - PTR(&x1[0].a), sizeof(x1[0]));
    printf("x2=%ld, %ld\n", PTR(&x2[1].a) - PTR(&x2[0].a), sizeof(x2[0]));
    printf("x3=%ld, %ld\n", PTR(&x3[1].a) - PTR(&x3[0].a), sizeof(x3[0]));
    printf("x4=%ld, %ld\n", PTR(&x4[1].a) - PTR(&x4[0].a), sizeof(x4[0]));
    printf("x5=%ld, %ld\n", PTR(&x5[1].a) - PTR(&x5[0].a), sizeof(x5[0]));
    printf("x6=%ld, %ld\n", PTR(&x6[1].a) - PTR(&x6[0].a), sizeof(x6[0]));
    printf("x7=%ld, %ld\n", PTR(&x7[1].a) - PTR(&x7[0].a), sizeof(x7[0]));
    printf("x8=%ld, %ld\n", PTR(&x8[1].a) - PTR(&x8[0].a), sizeof(x8[0]));
    printf("x9=%ld, %ld\n", PTR(&x9[1].a) - PTR(&x9[0].a), sizeof(x9[0]));
}

libcの移植

  • muslが有望?
  • 当初はnewlibかと思ったけど、configureが遠そう...
  • 他にはuclibとかNetBSD辺りのlibcとか...

chibiasで長さ不詳の配列のサポート

解決済み

  • C言語の用語で、Flexible arrayという。
struct foo {
 int a;
 int b[];   // <-- System.Int32_len0として生成しているが...
}
  • 専用の実装を用意する(対応済み: 0.28.0: 901c94124e7b8eb0)
    • 名前は、System.Int32_flexのようになる。
  • 意図的にIOBEを発生させない(対応済み: 0.28.0: 901c94124e7b8eb0)
    • 現在のインデクサ実装は、内部的にもポインタ計算でアクセスしているので、挙動としては望ましい(? 少なくともC言語同様にオーバーランアクセスを意図的に起こせる)
  • System.Int32_len0.Lengthがint.MaxValueを返すとか(対応済み: 0.28.0: 901c94124e7b8eb0 、publicなLengthを実装しない)
    • IList.Countの実装では、InvalidOpExNotImplExを投げるか。
  • System.Int32_flexは明示的にSize=0をつけたほうが良いかも。
    • メンバフィールドは定義されていないが、こういう構造体のsizeofオプコードが1になったような記憶がある...
    • やっぱり駄目だった。サイズが0にはならない。構造体にflexible arrayが含まれる場合は、メンバそのものではなくアクセサを公開するようにする(chibias)
    • chibiasの変更は結構面倒なので、いったん諦める。Flexible arrayを含む構造体を使うには、構造体へのポインタという形でしか使用しないと思われるので、その実体型のサイズが問題になることはほとんどない&ましてやC#などでそのシビアな型のインスタンスを直接取り扱うというシチュエーションが想像できない(Flexible array内の要素を参照することはありうるが、それは構造体へのポインタ経由 p->fa[123] しかありえない)

chibiasで関数シグネチャ表記をすべてコールサイト構文に変更する

  • シンプルなのが良いと思ったけど、関数シグネチャをサポートした関係でTypeParserに統合したほうが良いと思った。
  • call System.Console.WriteLine stringが、call System.String.WriteLine void(string)と書かなければならなくなる。
  • op_Implicitop_Explicitに対応できる(戻り値型を明示するため)
    • メソッド検索部分は戻り値型を検証していないので、対応する必要がある。
    • 割と最近?のCLRで、戻り値型の反変に対応したとか見た気がする。CLRの問題だったかRoslynの問題だったかは忘れた。
  • TypeParserで対応済み。戻り値型の反変には未対応。但しオーバーロード選択ではop_Implicitop_Explicit以外で戻り値の型を見てないため、コード自体は問題なく出力される(反変を満たす型でかつCLRが対応していれば動くと思われる)。

値を返す必要がない式(popして捨てるコードの排除)

解決済み: d0a78fd0fa418

  • gen_expr(Node *, bool discard) みたいにフラグをつける
  • gen_addr(Type *, bool discard) みたいにフラグをつけて、trueだったらldindしない。
  • trueで呼び出す側はpopしない
  • void result function(EVSに積まれてなければ、戻り値型がvoidなら何も考慮する必要はない?)

chibiasで、部分的にデバッグ情報を挿入させない方法

  • 自動生成されたCILソースコードをアセンブルしたい(生成したソースコードファイルは維持されないので失われるから)
  • .fileまたは.locationで指定させるか、ほかのディレクティブを追加する。
  • _start()の扱いと統合させる。
  • .hiddenを追加した。

mainエントリポイントの文字列の扱い

  • Marshal.StringToHGlobalAnsi()を使っているが、これは環境依存でUTF-8にならない疑い
  • Marshal.StringToCoTaskMemUTF8()というのが増えているが、.NET 6以上なので却下
  • _start()でちまちま実装するというのもあるが、いっそlibcを作るときにヘルパーメソッドを配置したほうが良いかも(そうするとlibc必須になるのでそれはそれでシンプルさは失われるので、どうするか)
  • エントリポイントは_entry()みたいな名前にして、直ぐに_start()を呼ぶ。_start()はlibcに配置しておくが、自分で作ってもいい(chibiasで参照時にlibcより先に見つかれば問題ない)、と言う事にするか。
  • 対応済み。常にEncoding.UTF8を使って真面目に実装した。デメリットとしてargvを要求すると、corlibへの参照が発生する(toolchainのドキュメントに書いた)

プリミティブ型変数への初期化が常に発生する

int add_all(int n, ...) {
  va_list ap;
  va_start(ap, n);

  int sum = 0;
  for (int i = 0; i < n; i++)
    sum += va_arg(ap, int);
  return sum;
}

の、sum = 0で、ND_MEMZEROと思われる初期化ノードが挿入される。

.locals (
  [0] int32*,
  [1] int32*,
  [2] int32 i,
  [3] int32 sum,
  [4] valuetype [mscorlib]System.ArgIterator ap
)

IL_000b: ldloca 3                          ; ND_MEMOZERO?
IL_000f: initobj [mscorlib]System.Int32    ; ND_MEMOZERO?
IL_0015: ldloca 3
IL_0019: ldc.i4 0
IL_001e: stind.i4

直後に0で初期値を代入しているので無駄。なんでこうなっているのか調べる必要がある。

  • 元々こういう動作かも?又は移植時のバグか?
  • ND_MEMZEROについてのコメントがあった気がする。0クリア初期化が要求されている(確か初期化式がある場合、という条件だったような気も)

InlineArray

今更... しかも後方互換性なし...

可変引数対応

CallingConvention.Varargに対応するのはWindowsだけというクソ仕様...

おまけに、netstandard2.0のアセンブリでTypedReferenceを使うと、CoreCLRがクラッシュするなど挙動がおかしい。 monoはすべてにおいて正しく動作する。

上記のような状況なので、ArgIteratorを使うのをやめて、__va_list クラスをlibcに用意して、これらのヘルパーを使うように変更してみたが、まだしっくり来ていない。また、chibildでPInvoke自動対応をやりたいので、このクラスを廃止して、Vararg前提のコードを出力するようにした方が良いのではないかと考えている。

  • 呼び出し先が.NETメソッド(関数を含む)である場合は、params object[] 相当の引数を受ける前提にする。 C#と親和性は高い。デメリット:
    • 呼び出し元は、コールサイトの指定が必須となる(可変引数部分を特定するため)。また、配列を生成するためのtrampoline又はshrinkerをchibild内で生成して、それを呼び出す必要がある。
    • chibiccはarglistOpCodeとArgIteratorを使って普通に書かせる。chibildでこれらのOpCodeやメンバを検出して、params object[]配列から値を取り出すコードに変換する(要難度検証)。
  • 呼び出し先がVarargやPInvokeメソッドである場合は、sentinel parameterを設定して、Varargに普通に対応させる。
    • chibiccが出力したコードはそのまま使用できる。
    • 現在の所monoかWindows CoreCLRでしか動かない。

params object[]に対応させるために勝手にコードを弄るのはかなりキモイが、PInvokeの場合に特別に対応させるよりも難易度が低そう(arglistArgIteratorなどの目立つ目印があるので)。 最後の制約は、これを読んでエンハンスが出来そうなら考える: https://github.com/jeremyVignelles/va-list-interop-demo

32/64バイナリ分離支援

まだラフなアイデア。AnyCPUのメンバオフセットをキャッシュするようにしたけど、それでも目に見えて32,64前提コンパイルした方が速くなる。 また、AnyCPUだと一部のマクロが定数にならないので、ソースコード互換性に影響がある。そこで:

chibicc -m32 -mproxy chibicc -m64 -mproxy みたいにして、32と64で別々のコンパイルを行う。 その後、chibicc -mproxy みたいにすると、AnyCPUのプロキシコードが出力出来て、これは実行時の32/64を判定して前述のどちらかのアセンブリに処理を自動的にバイパスする、みたいな感じ。

  • 関数はバイパスできそう。
  • グローバル変数とか型が怪しい。無理かも。
  • TypeForwardedToを動的にやれるような方法があると良いが... CoreCLRにはもしかしたらくちがあるかもしれないけど、OldCLRやmonoで出来ないならやらない。
  • 無理な場合は制約としたとしても、あると嬉しい機能かどうかは検討が必要。

未整理メモ

  • tagとtypedefによるシンボル名をCILに反映させる具体的な方法
    • tagがないパターン
      1. typedefされる場合: 初回のtypedef名を受け取る? 安定的か?
      2. 匿名型のインスタンス化: 完全にユニーク名でおk?
    • タグ名にはスコープがあるので、それをどうするか
      • スコープをCILでも実現する(C.text.type.foo, C.text.func1.type.foo? ちょっと無理があるかも)
      • ユニーク名。ユニークと言ってもpointer valueではdeterministicを確保できないのと、コンパイル単位が違っても予測可能でなければならない。但し、関数スコープ内であれば予測可能ユニーク名でなくても問題ない(外から見えないから)。CILメタデータ的には区別が必要。
      • スコープのネストにユニークインデックスをつける。インクリメンタル:(foo, foo1, foo2, foo3, ...), ツリー:(foo, foo1, foo1_1, foo1_2, foo2_1, ...)
    • 全く同じ定義の共通化
      • tagがないなら積極的にやりたい
      • tagがある場合は人間が区別したいと考えていると仮定して、同じでも別の定義にする?(C言語的に代入互換性があるかどうか?
  • MSBuild
    • .cprojとかやりたい。
    • むしろ、.csprojや.fsprojにプラグインできる方法もあったほうが良い(今のMSBuildのアンチテーゼとして)
    • chibicc.buildとは別に必要か。又は今のchibicc.buildを別の名前に変更して導入か。
    • 一から対応させる方法としては、MSBuildSdkExtrasが参考になるかも。
    • 「PackageReference を何ひとつ書かなくても NETStandard.Library を勝手にダウンロードしてくるのは、手軽にコードが書けるという点ではいいかもしれないけれど、やはり無駄なパッケージ参照があるというのは許せないものなので、消していきましょう。」 https://azyobuzin.hatenablog.com/entry/2017/03/23/032246

junk

mastodonで呟こうかとも思ったけど、ここに落としておく。

そういえば、stage2出来た後でchibiccの速度を比べると、圧倒的ですよgccベースのchibiccは... 多分10倍ぐらい違う。
というか、chibiccは最適化されていないコードを吐くとはいえ、CLRは実行時に何もしてないのかというぐらい遅い。
理由は調べてない(やるとしたら終わってから)けど、色々資料を見るにつけ、C#に最適化し過ぎではとは思ってる。
これを頑張ってC#が吐くコードぽく近づけたとしても、gccには遠く及ばないだろうなという推測も。
2~3倍ぐらいにまでは寄せれるかもしれないけど。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment