プログラミング
誰もがアセンブリは型がないと言うが、それは間違いだ
Everyone says assembly is untyped—everyone is wrong (gingerbill.org)
要約
この記事は、Odin言語のインラインアセンブリが、言語との深い統合、型チェック、統一された構文により、現時点で最も優れたシステムであると主張しています。著者は、文字列ベースや型システムとの連携が不十分な他の言語のアプローチと比較し、アセンブリは実際には型を持つ言語であり、型を尊重するインラインアセンブラが可能であることを論じています。
全文翻訳
TL;DR: Odinのインラインアセンブリは、現在どの言語よりも優れていると私は信じています。この記事の最も重要な側面は以下の通りです。これらの側面をすべて兼ね備えたアセンブリ(GCC/Clang/Rust/Go…)は、私の知る限り他にありません。
インラインアセンブリは、プロシージャのように呼び出し可能なasm「テンプレート」に整理されています。
asmテンプレートは、クロバー、ピン留め、タイ、スクラッチレジスタを指定するバインディングを通じて、コードの残りの部分と統合されます。
アセンブリ構文は、ISA間で統一されており、Odinの構文と一貫しています。
アセンブリは、Odinコードの他の部分と同様に、完全に型チェックされます。
アセンブリが実際には型を持つという理解。
コア:rexcodeエンコーディングテーブルを介したリアルなセマンティック診断。
これは約7日間で構築されました。
なぜOdinは独自のカスタムインラインアセンブラを持つことにこだわるのかとよく聞かれます。インラインアセンブリは解決済みの問題ではないか?文字列を取得し、アセンブラに渡し、残りはアセンブラに任せるだけだ。GCCからClang、Rustまで、Rustのインラインアセンブリはマクロシステムのおかげで少し洗練されていますが、それほど大差ありません。多かれ少なかれこれを行っています。車輪は発明されたのでしょう?これはまさに私が望まなかった設計であり、ほとんどの言語が落ち着いている設計です。
私の当初の目標は、言語の側面から切り離されたものではなく、言語の残りの部分と実際に統合されるインラインアセンブラでした。そして、Odinが現在持っているものが、現時点でどの言語よりも優れたインラインアセンブリシステムであると、私は率直に信じています。私はそれを軽々しく言っているのではなく、この記事の終わりまでには、少なくとも私がそう信じる理由を理解していただけることを願っています。
文字列ベースのナンセンス
まず、私が反発していたものから始めましょう。x86 AT&T/GAS構文を使用したGCCスタイルの拡張アセンブリにおける、簡単な「1を加える」の例を以下に示します。
int dst;
asm("movl %1, %0\n\t" "addl $1, %0"
: "=r" (dst) // 出力
: "r" (src) // 入力
: // クローバー
);
これを見て、自分自身に問いかけてください。コンパイラ(アセンブラではなく)は何を理解しているのでしょうか?答えは「ほとんど何も」です。本体は文字列です。「=r」と「r」は明示的な制約文字列であり、実際のDSLの横に貼り付けられた、もう一つの小さな文字列型DSLです。「%0」と「%1」は、手で数えなければならないリストへの位置参照です。もし間違った場合、得られるエラーは、あなたの型とセマンティクスを知っているコンパイラからのものではなく、アセンブラからのものであり、ずっと後になって、あなたが書いたものではない生成されたテキストを指し示します。
これは、機能が言語の一部としてではなく、最初にエスケープハッチとして設計された場合に起こることです。誰も「インラインアセンブリは、ホスト言語の型システム、呼び出し規約、定数システム、その他のもの(複数戻り値セマンティクスなど)を尊重するとしたら、どのように見えるだろうか?」と座って尋ねた人はいないようです。むしろ、「コンパイラの作業を最小限にして、この関数にアセンブリをどうやってねじ込むか?」と尋ね、その答えが文字列でした。このようなインラインアセンブラは、ホスト言語のすべての側面を無視し、ただねじ込むだけです。私はそうしませんでした。私はゼロから設計しました。
後付けの簡単な歴史
文字列だけがこの方法で行われたわけではありません。過去の言語/コンパイラが何をしてきたかを見る価値はあります。なぜなら、これらのアプローチのいくつかはGCC/Clangが行ったことよりもはるかに優れており、残念ながらコンパイラ分野ではこの開発は停止しているからです。
MSVC
MicrosoftのCコンパイラは、実際に異なるアプローチをとっていました。MSVCの__asmは、文字列ベースではなくステートメントベースでした。あなたは実際の命令のブロックを書き、そして(これが良い点ですが)Cの変数やラベルを直接名前で参照でき、コンパイラがそれらを解決してくれました。
int add_one(int x) {
__asm {
mov eax, x // 'x'はコンパイラによって解決されるCパラメータです
inc eax
}
// 呼び出し規約のために、eaxの値が戻り値です
}
制約文字列もなく、「%0」もなく、オペランドを参照するために数える必要もありません。GCCの仕組みと比較すると、これは率直に言って読みやすく、長らくWindowsのシステムコードのかなりの部分がこのように書かれていました。では、なぜそれは消えたのでしょうか?
第一に、それはx86専用でした。Microsoftがx64(および後のARM64)に移行したとき、彼らはそれを移植しませんでした。公式のガイダンスは、「コンパイラ組み込み関数を使用するか、別の.asmファイルを作成してMASMで実行する」となりました。x64コンパイラの明示的な制約の1つは、インラインアセンブラをまったく持たないことでした。アプローチ全体が、それを汎用化するのではなく、ISAの境界で捨てられました。
第二に、それが存在した場所でさえ、コンパイラはそのブロックを実際には理解していませんでした。シンボル名を解決しましたが、明示的なクロバー情報は持っていませんでした。オプティマイザは、保守的になるために、その領域を不透明なフェンスとして扱っていました。それはxが何であるかを知っていました。命令が何をしたかについて、ユーザーにフィードバックを提供しませんでした。
Turbo Pascal
さらに遡ると、Turbo Pascalが見つかります。私はパスカル全般と同様に、それに対して明らかな愛着を持っています。そのインラインアセンブリでは、2つのメカニズムがあり、それらを組み合わせることで、設計空間全体を非常にうまく網羅しています。
最初のメカニズムはinlineディレクティブであり、「コンパイラは何も理解しない」という最も純粋なステートメントです。あなたは機械語を数値定数のシーケンスとして与えました。実際のオペコードをバイトとして与えました。
procedure Cli;
inline($FA);
{ $FA = CLI命令 }
procedure Nops;
inline($90/$90);
{ 2つのNOPバイト }
それはアセンブラではありません。これはあなたが手作業でアセンブラになり、コンパイラが忠実にあなたのバイトをストリームにコピーするだけです。これは、Odinが#byteディレクティブとしてこの正確な機能を持っているur-escape-hatchですが、チェックされたテンプレート内の多くのディレクティブの1つとしてであり、インターフェース全体としてではありません。
2番目のメカニズムは、Turbo Pascal 6.0で追加された組み込みアセンブラでした。asm ... endブロックとassemblerプロシージャディレクティブです。このアプローチははるかに優れています。なぜなら、実際のニーモニックがあり、MSVCのように、Pascalの変数やパラメータを直接名前で指定できたからです。
function AddOne(X: Word): Word;
assembler;
asm
mov ax, X // 'X'はPascalパラメータです
inc ax // 結果はAXで返されます
end;
1990年当時としては、これは非常に素晴らしいように思えます。これは私の時代より前ですが(私はまだ生まれていませんでした)、現在のCコンパイラが最終的に到達した場所よりも進んでいたと言えるかもしれません。しかし、その時代の制約により、組み込みアセンブラは80286命令までしか理解できなかったため、386とその32ビットレジスタを必要とする日には、外部アセンブラに送られることになりました。後付けされ、そして閉じられました。
MSVCとTurbo Pascalは、GCCの設計と比較して、特に愚かな制約文字列に関して、その本能的な設計においてどちらも優れていました。しかし、どちらもまったく同じ場所で停止しました。識別子を解決しましたが、命令をモデル化することはありませんでした。オペランドの型もなく、即値範囲に対する限定的なチェックしかなく、何がクロバーされたか、または何がピン留めされる必要があるかについての制御もありませんでした。
GCCはそれらの設計を捨て、アセンブリが自身の言語で語る側面を忘れました。その下に型システムが存在し、アセンブリのために一般化できるという概念がありませんでした。これがOdinの設計の全体的なポイントであり、この記事の残りの部分で説明することです。
アセンブリは型がないわけではない
アセンブリは「型がない」という非常に一般的な信念があり、したがってインラインアセンブリは本質的に何でもありの事柄であると考えられています。これは真実ではなく、それを乗り越えることが、普遍化されたインラインアセンブラの設計全体における最も重要な考え方です。私は以前、Odinの文脈で「型のない型」について書きましたが、それらは実際には存在型です。一般的に、「型がない」とは、すべてが「不透明」で非常に弱い(例:すべてがintであり、どこでもそのように仮定する)ことを意味します。アセンブリは通常、そのような「型がない」言語の完璧な例と見なされます。しかし、すべての命令には有効な形式のセットがあります。各形式は、オペランドの種類(レジスタ、メモリ、即値…)を決定します。