生成されたコードを比較する最後に、IL2Cが生成するコードが、手でC言語コードを書いたものと比較してどの程度のフットプリントなのかを見てみます。比較対象は、ビルドするとgccによって生成されるネイティブバイナリです(デプロイ用パッケージではありません)。$(OutDir)/Mt3620App.outです。BlinkerのサイズがDebugとReleaseで同一なのは、間違いではありません。それだけ、コードの複雑性がなく、最適化の余地は無かったということでしょう。IL2CのStep1・Step2共に、最適化がかかった場合のフットプリント縮小が劇的です。これはIL2Cの設計上狙ったとおりの結果です。-Wl,–gc-sections)によって、使用されていないシンボルをリンク時に削除する事を前提としています。IL2Cのランタイムはまだ小規模ですが、やがてcorefxのマージを行うようになると、非常に大きくなることが予想されます。その場合でも、最低限必要なコードだけがバイナリに含まれて欲しいので、この前提が重要になります。他にも、C言語標準ライブラリをできるだけ使用する、という方針もあります。独自のコードを含めば含んだだけ、フットプリントが増えてしまうからです。System.Stringの文字列操作も、内部ではC言語のwcslenやwcscatなどの関数を使ったり、マークアンドスイープガベージコレクタが、mallocやfree関数をバックエンドとして使っている(そしてコンパクションを行わない)のも、このような背景によるものです。Blinkerのサイズと比較すると、やや大きいですね。努力が足りない感じです。フットプリントを小さくするアイデアは、まだ色々あるので、今後のバージョンでトライして行こうと思います。MCUを搭載したMT3620で、C#を使ってモダンなスタイルのコードを書くという試みを行いました。今回の試行でうまく動くようにするための技術的なフィードバックは、結構沢山出てきました(詳しくはコミットログを参照して下さい。難易度高くて、初見では意味不明かもしれませんが...)。ドッグフーディングはやはりやっておくべきですね。IL2Cのような試みは他でもいくつか行われていて、LLVM向けのものとか、Roslynの構文木からC言語のコードを生成するとか、はてはC#でブート可能なOSを作る、なんてものもあります(ググってみて下さい)。Core・monoとの違いとしては、ポータビリティとフットプリントかなと思っています。完成には遠い感じですが、これからも、ただの概念実験にならないようにしていきたいと思います。クリエイトベース金山さんのスペースでやりました。冬場だからか、自分も含めて体調良くない人が居るようですね...私はIL2Cのオブジェクト参照の追跡が足りない部分の修正と本を読む、MatsuokaさんはnanoFrameworkとMbed-Cli、neco3csさんはAngularの勉強、中村さんはNGK2018BのLT資料作成、加藤さんは非公開案件、という目標でした。対処する、その方策を考えました。今日中に対処できれば良いんですが、そう簡単でも無いことは分かっていたので、今日は算段をつけるという感じ。別の参照を上書かせて追跡されないようにしてテストしている。}}で、ObjRefInsideValueType()を呼ぶとこういったケースのテストを行うのですが、構造体ObjRefInsideValueTypeType内のValueフィールドにセットされた文字列(System.String.Concatによって動的に生成されてヒープに配置された文字列のインスタンス)が、GC.Collect()によって回収されてしまうという問題です。constに配置されてしまってGCから無視されてしまうので、動的に生成させています。無効な値を示していて、その後のアサートで刺さるからテストに失敗する、というシナリオです。テストが成功する...で、VC++で実際にデバッグしてみると、問題なく無効ポインタで刺さったことが検出されます。32bitを使用)してネイティブの実行コードを生成する(今の所Windowsでやってるので、test.exe的なものが生成される)実行コードを実際に実行する。CLRは一切関与しない)。成功時は”Success”とstdoutに出しているので、それを確認という感じで、テスト結果が完全に一致することを自動的に確認するようにしています。extensionでGDBを使ってデバッグできる)してみたのですが、こちらも正しく無効ポインタを踏んで死んでました。調べてるところで時間切れ。嫌な感じだな...Try!開発は定期的にやる予定なので、興味があれば是非参加してください。クリエイトベース金山さんのスペースを借りることが出来ました。募集が遅かったので人の集まりが悪かったのですが、参加者3人ともいろいろ吸収出来たようで良かったです。私はIL2Cの例外処理、Matsuokaさんは次週のプレゼン資料、ayumaxさんはUnityで年末忘年会用の出し物を作る、という目標でした。IL2Cは例外処理に取り組んでいます。unwindは動いていて、finallyを変換可能にするのが今日の目標でしたがダメでした。この図はIL内にtry-catch-finallyブロックの定義があった場合に、どのようなCのソースコードに変換すれば実現できるかを検討していたものです。言うと、sjlj方式でsetjmpの戻り値によって例外ブロックに分岐して処理させ、finallyはそれらがbreakでブロック外に遷移したときに面倒を見る、という感じです。(sjljの効率が悪いことは承知。6で追加された例外フィルタ条件のサポートです。1.0から使える(つまりはIL的には最初から実現可能だった)ものです。GetMessage()を呼び出しているwhen句)の呼び出し順序を見て、のけ反る人もいるかも知れません。コメントに書いておきました。現在のスタックフレームの直近に存在するcatchブロックのうち、指定された例外型にキャスト可能なものがあるかどうかを探索し、なければよりスタックの底に向かって探索し続ける。上記が存在する場合、更にフィルタ条件式があればそれを実行し、結果が満たされるかどうかを確認する。1又は2が満たされてから、そのcatchブロックに遷移する。いう順序で実行する必要があります。ここで苦しいのは、スタックの底まですべての条件を確認し終えるまでは、catchブロックに遷移してはならない、という点です。したがって、まず、catch句の条件とwhenのフィルタ条件をすべてチェックする必要があります(満たされるものが見つかった時点で打ち切ることは可能)。問題なのは、例外の型のチェックだけなら、IL2Cが自分の都合の良い判定Cコードを生成すればいいのですが、フィルタ条件式はILで指定されるので、ILを実行してフィルタ条件を満たしているかどうかを確認しなければなりません。catchブロックやfinallyブロックは、そのブロックを実行することが判明した時点でそこまでスタックを巻き戻し(sjljであればlongjmpする)ても問題ないことが自明ですが、フィルタ条件式の実行中は、まだスタックを巻き戻すことは出来ません。巻き戻してしまったらもう元に戻せず、それまでのスタック上の情報はすべて失われます(移植性を考えると、失われたと見なす必要があります)。(上記のサンプルコードはまだ良いのですが、メソッドのローカル変数を使ったフィルタ条件式を考えてみると、問題の大きさがわかります)これは頭を抱えました。Console.WriteLine("A");}しかし、この方法には2つの問題があります:例外ハンドラに遷移してから、更にrethrowで遷移するので、コストが高い。同じ型でcatchするブロックについて、一つのブロックで処理するように統合する必要がある。これもやっぱり駄目ですね。スタック上位にfinallyブロックがある場合、このcatchに遷移した時点でfinallyを実行してしまいます。例外フィルタ式を使った場合と、実行順序が入れ替わってしまいます。“EXECUTION_FRAME”という構造体に記録し、リンクリストに追加することでGCがトラッキングできるようにしていますが、ここの実行効率が悪いことがわかっています。近い内にこれを改良する予定ですが、その際にこれらのローカル変数へのベースとなるポインタが用意に得られるようになるはずなので、フィルタ条件式のILを変換する場合には、このポインタ経由でローカル変数群にアクセス可能にすれば、スタックを一時的に巻き戻さなくとも式の評価が出来るはずです。そのため、手順として、EXECUTION_FRAMEの改良が終わってから、改めて考えても良いかも?と考えました。Core、monoとかはどうやっているのかという興味が出てきますね。自力でネイティブコードを出力してるので、如何様にでも出来るといえば出来ますが...——-Try!開発は定期的にやる予定なので、興味があれば是非参加してください。2017」の11日目のネタです。いうプロジェクトをやっているのですが、その実装中に起きた予期せぬフロー解析を、素人なりに実装した話です。C言語ソースコードに変換します。2017」でもこれまでの内容を要約して書いてます。この記事はダブルエントリーにしました。:)※本記事は抜粋ですが、.NETにあまり明るくない方向けに、細部を省いて焦点に絞って編集してあるため、省略した解説があります。—(ゲームエンジンの)Unityには、”IL2CPP”というツールがあります。Language)を、C++のコードに変換するツールです。ILとは、JavaのバイトコードやLLVM-IRに相当する、.NETの中間言語です。“IL2CPP”と聞けば、ILのバイトコードを(多少面倒ではあるものの)パースし、一対一でC++のコードに変換していけば完成し、後は、ランタイムをどう実装するのかが大きなトピックだろうと想像できます。プリミティブタイプは、C++の対応する型に置き換えればよいでしょう。CLRが扱うクラスや構造体の型は、C++のクラスにマッピング出来そうな気がします。型全体について、System.Object(.NETにおける基底クラス)と同じような基底クラスを軸に、C++でも継承関係を構築すれば、例外クラスもほぼそのまま変換できるように思えます。C++にはtry-catchが存在するため、例外ハンドリングも楽に実装できるかもしれません。C++で記述できるコードは、多少冗長になったり面倒になったりしますが、C言語だけでも実装できることを知っています。ツール化によって自動生成するのであれば、C言語で出力することは大した問題ではありません。—しかし、IL2Cの設計を進めていくうちに、型の扱いがそれほど簡単でもないことに気が付いた...立ちはだかる様々な問題のうち、全く想定していなかった「フロー解析」についての知見を残しておきます。CLRには仮想計算器が定義されています。この計算器(CPUと読み替えてよい)は、ILのバイトコードを逐次解釈しながら、OpCodeの定義に従った計算を実行します。CLRの仮想計算器は「スタックマシン」と呼ばれている種類のアーキテクチャです。スタックマシンとは、仮想計算器が使用するスクラッチパッドに、文字通り「スタック」構造のバッファを使います。このスタックのことを「評価スタック」と呼んでいます。(int64_t)stack.pop_back();}評価スタックとは「スタック」と呼ばれるように、LIFO構造のスクラッチパッドです。ポイントは、評価スタックが「スタック」ゆえに、必ずスタックに積まれた順序を守りながら操作するということです。この点は、一般的な物理CPU、たとえばx86・x64・ARMなどのCPUと異なります。これらのCPUは「レジスタ」と呼ばれるスクラッチパッドを使い、評価スタックのようにLIFOであることを想定しません。評価スタックにはどのような値も入れることが出来ます。プリミティブ型・値型・オブジェクト参照・マネージ参照などです。以下に評価スタックの処理を示します:aの値を評価スタックにプッシュbの値を評価スタックにプッシュ2つの値を取り出し(ポップ)ながら加算処理を行い、結果をプッシュcの値を評価スタックにプッシュ2つの値を取り出し(ポップ)ながら加算処理を行い、結果をプッシュ値を取り出し(ポップ)、呼び出し元に返す評価スタックの正体このC++コードには、明らかに問題のありそうな実装が含まれています。(int64_t)stack.pop_back();}まず、std::stackのテンプレート型引数”object”が解決できません。C++にはSystem.Object(.NETにおける基底クラス)に対応するクラスが存在しないため、このコードは成り立ちません。+()が解決できないという問題も存在します)仮に、全ての型の基底となるクラスを導入すれば解決できそうな気もします(同程度の新たな問題も発生しますが、ここでは述べません)。私達の目標はC++ではなくCです。C言語で上記のコードを模倣する場合、次の方法が考えられます:std::stackと等価な、テンプレートクラスベースではないスタック操作関数を用意するこの方法は、スタックポインタの操作(現在のスタック位置を把握する)を実装する必要があり、Cコンパイラが最適化によってこれらを不要と検出できるかどうかが鍵となります(実際のところ、スタックポインタの計算処理が含まれるため、難しいかもしれません)。結局、System.Object型をC言語でどのように扱うのかが解決されません。スタックに値が2個しか出し入れされていない前提で、それぞれをローカル変数に置き換える「人間の目」で確認すれば、評価スタックのインデックス(スタックに積まれた位置)毎に、何型の値を格納しているのかがわかります。stack0_1;評価スタックの使われ方を図示すると、次のようになります:これで、評価スタックをCコードで問題なくシミュレートできたことになります。出力されるコードの問題自体は解決しましたが、IL2Cの変換処理は非常に複雑になります。当初の想定では、リフレクションAPIで取得できるメタデータ情報とILのバイトコードを、逐一Cコードに置き換えるだけでした。メソッドの引数群・戻り値・ローカル変数群の型は、Typeクラスのインスタンスによって直接かつ容易に特定できます。評価スタックの操作だけは、型が明示的に指定されない(PushやPop時に型指定されない)ため、PushやPop操作を追跡し、どこでどのスタック位置をどのように使っているか、を解析しなければならないのです。評価スタック解析の方法ILのバイトコードを上から順に解析するのではなく、評価スタックの操作を追跡して解析しなければならないとすると、実際に仮想計算器が解析を行う挙動を模倣する必要があります。具体的には、バイトコードのエントリポイント(先頭)から順に解析を行いつつ、OpCodeのPushやPopの操作を追跡する(その際に、Pushされた型を記憶する)ブランチ命令を発見した場合は、ブランチ先を新たなフローとしてキューに保存するret命令や、解析済みのバイトコードに到達した時点で、そのフロー解析を終了するという動作を、キューを全て消費するまで繰り返します。ここで重要なのは、評価スタックの使用方法のチェックです。ブランチ命令によって遷移する先のバイトコードは、すでにフローとして解析済みの可能性があります。その場合、そこから先で使用(Pop)する予定のスタックの型が、ブランチ直前のスタックの型と一致していれば、Cコード上でもまったく同じコードが使用可能であるとみなせます。スタックにPushされている値の型が異なる場合、ブランチ元で想定される型で新たにCコードを出力しなければなりません。intとは、”System.IntPtr”のことです。これは32ビット環境では32ビット値、64ビット環境では64ビット値を保持します。intをサポートしていません)ここで問題なのは、(B)で計算されるadd命令が、初回は(A)により3)であり、次回以降が4)として処理されることです。いったコードはC#では書くことができませんが、ILとしては有効です。intと変わっていること(C)のブランチが条件を満たさない場合の、後続のフローが存在することを判断し、特に2)によって、「似たような、しかし異なるCコードを生成する必要」を判断します。C言語では、ローカル変数に型を指定しなければなりません。同じスタック位置でも異なる型を必要とする場合は、その変数を使用する全ての箇所で、ソースコードを再生成する必要があります。(F)の部分は完全に同一なので、ラベルとgotoを駆使することで共通化できる可能性があります。(現在のIL2Cはその検知を行っていません。判定は複雑ではなさそうですが、難易度は高いと思います。ILのメタデータには、.NETの型情報が全て含まれています。リフレクション(やメタデータを解析するサードパーティのライブラリ)を使用すれば、型の情報は容易に引き出すことが出来ます。これによって、フィールドの型・メソッドの戻り値の型・メソッド引数群の型・ローカル変数群の型など、あらゆる型を直接特定できます。評価スタックの各スロットに格納される値の型については、どのような型が格納されるのかを静的に定めることが出来ません。IL2Cでは、IL内での評価スタックの使われ方を解析して、評価スタックのスロットにどんな型を想定しうるかを調べています。その情報を元に、完全に静的に定める必要のあるC言語向けに、型ごとのコード生成を行っています。当初はこのような事を考えていなかったので、必要性を知ったときには「これは大変そうだ」と思いましたが、何とか乗り切ったので良かったです。2017」の10日目です。いうか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。IL2Cのダイジェストをやっていこうかと思います。unpluged枠だったので、スライドは殆ど無く、喋りだけです。この時にも幾つか過去のビデオを見直していたのですが、やっぱりフロー解析の辺りが一番大変でしたね。typeのインスタンスフィールド・メソッドの対応を行います。インスタンスフィールドにアクセスするにはldfldやstfldを使えばよいのですが、インスタンスメンバにアクセスするには、インスタンスへのアドレス(マネージ参照)が必要です。MSDNではマネージ参照のことを「アドレス」と呼んでおり、かなり違和感があります(アンマネージポインタのようだ...)。typeのメンバにアクセスする場合は、マネージ参照を評価スタックにpushすれば良いため、ldlocaを使ってローカル変数へのマネージ参照をスタックにpushしておきます。実はメンバを参照するだけ(例えばフィールドから値をldfldで取得する)であれば、マネージ参照ではなく値そのものでも問題ありません。値をストアする(stfld)場合は、必ずマネージ参照をpushする必要があります。これはちょっと考えると当たり前で、スタックに格納されている値型のフィールドを変更しても、それはスタックに保持されているので、すぐに破棄されてしまいます。typeのフィールドアクセスに必要なILに対処しました。ldloca.s・initobj・stfld命令のILConverterを実装します。口に出していませんが、この辺から、評価スタックに付けているマングリングされたシンボル名がやたら長くなるのが気になってきました。2017」の9日目です。いうか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。IL2Cのダイジェストをやっていこうかと思います。(イメージをクリックするとYouTubeが開きます)#6-25アセンブリ・モジュール・型・メソッド・メソッドボディの関係を説明しました。対象のモジュールを間違えていました。tokenはそれが定義されているモジュールで一意なので、正しいモジュールからResolveする必要があります。__stack0_int32_t;}第二引数はint16であり、評価スタックが常にint32以上となるので、明示的にキャストを挿入しています。type内のスタティックフィールドの変換です。.NETでのスタティックフィールドとは、一意なインスタンスの格納先となるのですが、C言語に対応するものは「グローバル変数」です。えぇ、「グローバル変数」ですよ。:)C言語でグローバル変数を実現する場合にも、staticキーワードを付けることが出来ます。このキーワードは、シンボルをファイルスコープに制約するもので、(C#構文と似ているのに).NETとは似ても似つかない感じですね。typeに対応する定義までを行いました。typeのスタティックフィールドに対処しました。initializerの問題に気が付きました。ここで示した方法は、リフレクションでスタティックフィールドの初期値を取り出していますが、この方法には重大な問題があります。initializerが実行されてしまうということです。これは結構厄介な問題で、現在(#50)の時点ではissueに積んで保留しています。2017」の8日目です。いうか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。IL2Cのダイジェストをやっていこうかと思います。(イメージをクリックするとYouTubeが開きます)#6-2216ビットプリミティブの対応を行いました。8ビットとほとんど同じ対応で行けました。ldc.i2が存在しないので、果たしてリテラル16ビット値がどのように扱われるのか、ちょっと興味がありました。32ビットに拡張され、切り捨てられるので問題ないのような最適化(というほどのものでもありませんが)がRoslynで行われていました。ための準備を行いました。il2c.exeを放置していたので、これを最低限のツールとしての体裁を整えます。ツールとして、コマンドラインのオプションを受け取れるようにするのも課題ですが、コマンドラインからメソッドの実体を注入することは出来ないため:コマンドラインに対象のアセンブリファイルを指定するアセンブリに含まれる全てのメソッドを変換の対象とするメソッド名は、名前空間と型名を含めた、マングリングされた関数名に変換する結果を単一のCソースファイルとして出力するというように決めました。ここで初めて、マングリングされたシンボル名が具体的にどんな具合なのかを見ました。シンボル名解決のためにC++を使いたいなーという感想。typeに着手しました。メンバー(フィールド・メソッド)とスタティック・インスタンスの組み合わせによる定義を変換可能にします。typeであれば、メンバーの構成方法・型の定義方法ぐらいです、それでもかなり大掛かりですが...まずは、スタティックメソッドの呼び出しを処理させます。tokenからMethodInfoを取得して、対応する関数呼び出し式に変換します。スタティックなので、callvirtではなくcall命令を使います。token値からMethodInfoを入手できます。ここから、関数呼び出しのソースコードを構成します。メソッド呼び出しのパラメータ群は、評価スタックに積まれているので、これを逆順で取り出します。DecodeContextには評価スタックに相当する式をシンボル名(”__stack0′′のような)で取得できるので、これをカンマで結合すれば、関数の引数群を構成できます。最後に、少しだけALM(ビルドシステムとしてどこからどこまでどのように実現するか)の事に触れています。Fifteen.%d人のブロガーが「いいね」をつけました。