私はWebアセンブリの取り組みに関与していないことを指摘したいと思いますが、
そして、私は大規模または広く使用されているコンパイラを維持していません(私自身のものだけです)
おもちゃっぽい言葉、QBEコンパイラバックエンドへのマイナーな貢献、そして
IBMのコンパイラチームでのインターンシップ)、しかし私は少し怒りを覚えてしまいました、そして
より広く共有することが奨励されました。
だから、私は少し不快ですが、飛び込んで大きな変更を提案します
私が取り組んでいないプロジェクトに...ここに行きます:
私がコンパイラーを書いているとき、私が最初にすることは高レベルで行うことです
構造体(ループ、ifステートメントなど)は、セマンティクスについて検証します。
タイプチェックなどを行います。 私が彼らと一緒に行う2番目のことはただそれらを投げることです
アウトし、基本ブロックにフラット化し、場合によってはSSAフォームにフラット化します。 他のいくつかの部分で
コンパイラの世界では、一般的な形式は継続渡しスタイルです。 私は違います
継続渡しスタイルでのコンパイルの専門家ですが、どちらもそうではないようです
Webアセンブリが持っていると思われるループとスコープブロックに適している
抱きしめた。
よりフラットなgotoベースのフォーマットの方がはるかに便利であると主張したいと思います。
コンパイラ開発者のターゲットであり、
使用可能なポリフィルの作成。
個人的には、ネストされた複雑な式の大ファンでもありません。 彼らは少しです
特に内部ノードに副作用がある場合は、消費するのが面倒ですが、私は
コンパイラの実装者として彼らに強く反対しないでください-Webアセンブリ
JITはそれらを消費することができます、私はそれらを無視してマップする命令を生成することができます
私のIRに。 彼らは私がテーブルをひっくり返したくはありません。
より大きな問題は、ループ、ブロック、およびその他の構文要素にあります
それは、最適化コンパイラライターとして、あなたは非常に一生懸命に表現しようとします
エッジを表すブランチのあるグラフ。 明示的な制御フローの構成
障害です。 実際に行ったら、グラフからそれらを再構築します
必要な最適化は確かに可能ですが、それはかなりの量です
より複雑なフォーマットを回避するための複雑さ。 そしてそれは私を悩ませます:両方
生産者と消費者は完全に発明された問題に取り組んでいます
これは、複雑な制御フロー構造を単に削除することで回避できます
Webアセンブリから。
さらに、より高いレベルの構成の主張は、いくつかにつながります
病理学的症例。 たとえば、Duff'sDeviceはひどいウェブになってしまいます
The WasmExplorerをいじって見たアセンブリ出力。
ただし、その逆は当てはまりません。表現できるすべてのもの
Webアセンブラでは、いくつかの同等のものに簡単に変換することができます
構造化されていない、gotoベースのフォーマット。
したがって、少なくとも、Webアセンブリチームが追加することを提案したいと思います
任意のラベルとgotoのサポート。 彼らがより高いままにすることを選択した場合
レベルの構成、それは少し無駄な複雑さになるでしょうが、少なくとも
私のようなコンパイラライターは、それらを無視して出力を生成することができます
直接。
これについて話し合うときに聞いた懸念の1つは、ループが
ブロックベースの構造により、ウェブアセンブリのポリフィリングが容易になります。
これは完全に間違っているわけではありませんが、単純なポリフィルソリューションだと思います
ラベルとgotosが可能です。 あまり最適ではないかもしれませんが、
バイトコードを順番に少し醜くする価値があると思います
技術的負債が組み込まれた新しいツールを開始しないようにするため。
Webアセンブリの構文のようなLLVM(またはQBE)を想定している場合、いくつかのコード
それは次のようになります:
int f(int x) {
if (x == 42)
return 123;
else
return 666;
}
コンパイルされる可能性があります:
func @f(%x : i32) {
%1 = test %x 42
jmp %1 iftrue iffalse
L0:
%r =i 123
jmp LRet
L1:
%r =i 666
jmp LRet
Lret:
ret %r
}
これは、次のようなJavascriptにポリフィルすることができます。
function f(x) {
var __label = L0;
var __ret;
while (__label != LRet) {
switch (__label) {
case L0:
var _v1 = (x == 42)
if (_v1) {__lablel = L1;} else {label = L2;}
break;
case L1:
__ret = 123
__label = LRet
break;
case L2;
__ret = 666
__label = LRet
break;
default:
assert(false);
break;
}
}
醜いですか? うん。 それは重要ですか? うまくいけば、Webアセンブリが離陸した場合、
長くはない。
ええと、もし私がWebアセンブリをターゲットにすることに取り掛かったとしたら、私はコードを生成すると思います
ポリフィルで述べたアプローチを使用して、すべてを無視するように最善を尽くします
コンパイラが十分に賢いことを期待して、高レベルの構造
このパターンに追いつきます。
しかし、コード生成の両面が必要でなかったらいいのにと思います
指定された形式を回避します。
@oridb Wasmは、コンシューマーがSSA形式にすばやく変換できるようにある程度最適化されており、構造は一般的なコードパターンに役立ちます。したがって、構造は必ずしもコンシューマーの負担ではありません。 「コード生成の両側が指定された形式を回避する」というあなたの主張に同意しません。 Wasmは、スリムで高速な消費者に関するものであり、スリムで高速にするための提案がある場合は、建設的なものになる可能性があります。
DAGに順序付けできるブロックは、例のように、wasmブロックとブランチで表現できます。 スイッチループは必要に応じて使用されるスタイルであり、おそらく消費者はここで役立つジャンプスレッドを実行する可能性があります。 おそらく、コンパイラのバックエンドで多くの作業を行う可能性のあるbinaryenを見てください。
より一般的なCFGサポート、および言及されたループを使用する他のいくつかのアプローチに対する他の要求がありましたが、おそらく現在、焦点は他の場所にあります。
エンコーディングで「継続渡しスタイル」を明示的にサポートする計画はないと思いますが、引数をポップし(ラムダのように)、複数の値をサポートし(複数のラムダ引数)、 pick
演算子を使用すると、定義(ラムダ引数)を簡単に参照できます。
この構造は、一般的なコードパターンに役立ちます
Webアセンブリが強制する制限されたループとブロックのサブセットと比較して、任意のラベルへの分岐の観点から表現しやすい一般的なコードパターンは見当たりません。 コードを特定のクラスの言語の入力コードに近づけようとすると、わずかなメリットが見られますが、それは目標ではないようです。
DAGに順序付けできるブロックは、例のように、wasmブロックとブランチで表現できます。
はい、できます。 ただし、この方法で表現できるものと、追加の作業が必要なものを決定するために、追加の作業を追加しないことを強くお勧めします。 現実的には、余分な分析をスキップし、常にスイッチループフォームを生成します。
繰り返しますが、私の主張は、ループとブロックが物事を不可能にするということではありません。 それは、マシンがgoto、goto_if、および任意の非構造化ラベルを使用して作成するために、彼らができることすべてがより単純で簡単であるということです。
おそらく、コンパイラのバックエンドで多くの作業を行う可能性のあるbinaryenを見てください。
私はすでにかなり満足しているサービス可能なバックエンドを持っており、コンパイラ全体を自分の言語で完全にブートストラップする予定です。 ループ/ブロックの強制的な使用を回避するために、かなり大きな余分な依存関係を追加したくありません。 単純にスイッチループを使用する場合、コードを発行するのは非常に簡単です。 Web Assemblyに存在する機能を実際に効果的に使用しようとすると、それらが存在しないふりをするのではなく、かなり不快になります。
より一般的なCFGサポート、および言及されたループを使用する他のいくつかのアプローチに対する他の要求がありましたが、おそらく現在、力は他の場所にあります。
ループに利点があるとはまだ確信していません。ループで表現できるものはすべてgotoとlabelで表現でき、フラットな命令リストからSSAへの高速でよく知られた変換があります。
CPSに関する限り、明示的なサポートは必要ないと思います。直接アセンブリに変換するのはかなり簡単で、推論の点でSSAに同様の利点があるため、FPサークルで人気があります(http:// mlton.org/pipermail/mlton/2003-January/023054.html); 繰り返しになりますが、私はその専門家ではありませんが、私が覚えていることから、呼び出しの継続は、ラベル、いくつかのmovs、およびgotoに下げられます。
@oridb 'フラットな命令リストからSSAへの高速でよく知られた変換があります'
それらがwasmSSAデコーダーとどのように比較されるかを知ることは興味深いでしょうか、それは重要な質問ですか?
Wasmは現在、値スタックを利用していますが、その利点のいくつかは構造がないと失われ、デコーダーのパフォーマンスが低下します。 値スタックがないと、SSAデコードにも多くの作業が必要になります。レジスタベースコードを試しましたが、デコードが遅くなりました(それがどれほど重要かはわかりません)。
値のスタックを維持しますか、それともレジスタベースの設計を使用しますか? 値のスタックを維持する場合、おそらくそれはCILクローンになり、おそらくwasmのパフォーマンスをCILと比較することができますが、誰かが実際にこれをチェックしていますか?
値のスタックを維持しますか、それともレジスタベースの設計を使用しますか?
実はそんなに強い気持ちはありません。 エンコーディングのコンパクトさが最大の懸念事項の1つになると思います。 レジスターの設計は、そこではうまくいかない場合があります。または、gzipで素晴らしく圧縮される場合があります。 私は実際に頭のてっぺんからはわかりません。
パフォーマンスは別の懸念事項ですが、バイナリ出力をキャッシュする機能に加えて、ダウンロード時間がデコードを桁違いに上回る可能性があることを考えると、それほど重要ではないと思います。
それらがwasmSSAデコーダーとどのように比較されるかを知ることは興味深いでしょうか、それは重要な質問ですか?
SSAにデコードしている場合、それは、妥当な量の最適化も行っていることを意味します。 そもそも、デコードのパフォーマンスがどれほど重要かをベンチマークしたいと思います。 しかし、はい、それは間違いなく良い質問です。
ご質問やご不明な点をありがとうございます。
の設計者や実装者の多くが注目に値します
WebAssemblyには、高性能の産業用JITだけでなく、そのバックグラウンドがあります。
JavaScript(V8、SpiderMonkey、Chakra、JavaScriptCore)の場合だけでなく、
LLVMおよびその他のコンパイラ。 私は個人的にJava用に2つのJITを実装しました
バイトコードと私は無制限のgotoを備えたスタックマシンであることを証明できます
デコード、検証、および構築にかなりの複雑さが導入されます
コンパイラIR。 実際、Javaで表現できるパターンはたくさんあります
C1とC2の両方を含む高性能JITを引き起こすバイトコード
HotSpotは、コードをあきらめて、
通訳者。 対照的に、コンパイラIRを次のようなものから構築する
JavaScriptまたは他の言語からのASTも私がやったことです。 The
ASTの追加の構造により、この作業の一部がはるかに簡単になります。
WebAssemblyの制御フロー構造の設計により、消費者は次のように簡素化されます。
SSAフォームへの迅速でシンプルな検証、簡単なワンパス変換を可能にします
(グラフIRでも)、効果的なシングルパスJIT、および(postorderと
スタックマシン)比較的単純なインプレース解釈。 構造化
制御により、還元不可能な制御フローグラフが不可能になり、
デコーダーとコンパイラーのための厄介なコーナーケースのクラス全体。 また
V8のWASMバイトコードで例外処理のステージをうまく設定します
すでに生産と協調してプロトタイプを開発しています
実装。
これについては、メンバー間で多くの社内ディスカッションを行ってきました。
トピック、なぜなら、バイトコードの場合、それはと最も異なるものの1つだからです
他のマシンレベルのターゲット。 ただし、ターゲティングと違いはありません
JavaScript(最近多くのコンパイラが行う)のようなソース言語と
構造を実現するために必要なのは、ブロックのわずかな再編成だけです。 三
これを行うための既知のアルゴリズムとツールです。 いくつか提供したい
任意のCFGから始めて、これらのプロデューサーのためのより良いガイダンス
これをよりよく伝えます。 ASTから直接WASMをターゲットとする言語の場合
(これは実際には、V8がasm.jsコードに対して直接行うことです。
JavaScript ASTをWASMバイトコードに変換する)、再構築はありません
必要なステップ。 これは多くの言語ツールに当てはまると予想しています
内部に洗練されたIRがないスペクトル全体。
2016年9月8日木曜日午前9時53分、オリ・バーンスタイン[email protected]
書きました:
値のスタックを維持しますか、それともレジスタベースの設計を使用しますか?
実はそんなに強い気持ちはありません。 想像します
エンコーディングのコンパクトさは最大の懸念事項の1つです。 あなたとして
述べたように、パフォーマンスは別です。それらがwasmsSAデコーダーとどのように比較されるかを知ることは興味深いでしょう、それは
重要な質問ですか?SSAにデコードしている場合、それはあなたが
妥当な量の最適化。 どのようにベンチマークするのか興味があります
そもそも、重要なデコードパフォーマンスがあります。 しかし、はい、それは
間違いなく良い質問です。—
このスレッドにサブスクライブしているため、これを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/WebAssembly/design/issues/796#issuecomment -245521009、
またはスレッドをミュートします
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
。
@titzerに感謝し
分岐予測を改善するために還元不可能な制御フローに依存する通訳者が使用するこのきちんとした最適化があります...
@oridb
よりフラットなgotoベースのフォーマットの方がはるかに便利であると主張したいと思います。
コンパイラ開発者のターゲット
私は、gotosが多くのコンパイラーにとって非常に役立つことに同意します。 そのため、Binaryenのようなツールを使用すると、gotosを使用して任意のCFGを生成でき、それを非常に迅速かつ効率的にWebAssemblyに変換できます。
WebAssemblyは、ブラウザーが使用するために最適化されたものと考えると役立つ場合があります( @titzerが指摘したように)。 ほとんどのコンパイラは、おそらくWebAssemblyを直接生成するのではなく、Binaryenのようなツールを使用する必要があります。これにより、gotoを発行し、無料で多数の最適化を取得でき、WebAssemblyの低レベルのバイナリ形式の詳細について考える必要がなくなります(代わりに)単純なAPIを使用してIRを発行します)。
while-switchパターンを使用したポリフィルについて:emscriptenでは、ループを再作成する「relooper」メソッドを開発する前に、その方法で開始しました。 while-switchパターンは、平均で約4倍遅くなります(ただし、場合によっては、大幅に少ないまたは多い場合があります。たとえば、小さなループの方が感度が高くなります)。 理論的にはジャンプスレッドの最適化によって速度が向上する可能性があることには同意しますが、一部のVMは他のVMよりも優れているため、パフォーマンスの予測は難しくなります。 また、コードサイズの点でも大幅に大きくなっています。
WebAssemblyは、ブラウザーが使用するために最適化されたものと考えると役立つ場合があります( @titzerが指摘したように)。 ほとんどのコンパイラは、おそらくWebAssemblyを直接生成するのではなく、Binaryenなどのツールを使用する必要があります...
この側面がそれほど重要になるとはまだ確信していません。繰り返しになりますが、バイトコードのフェッチのコストがユーザーに表示される遅延の大部分を占めると思われます。2番目に大きなコストは、解析と検証ではなく、実行される最適化です。 。 また、バイトコードが破棄され、コンパイルされた出力がキャッシュされるものであると想定/期待しているため、コンパイルは1回限りのコストで効果的になります。
しかし、Webブラウザーの消費を最適化する場合は、WebアセンブリをSSAとして定義してみませんか。これは、私が期待するものとより一致し、SSAに「変換」する労力が少ないように思われます。
ダウンロード中に解析とコンパイルを開始できます。一部のVMは、事前に完全なコンパイルを実行しない場合があります(たとえば、単純なベースラインを使用する場合があります)。 そのため、ダウンロードとコンパイルの時間が予想よりも短くなる可能性があり、その結果、解析と検証が、ユーザーに表示される合計遅延の重要な要因になる可能性があります。
SSA表現に関しては、コードサイズが大きくなる傾向があります。 SSAはコードの最適化には最適ですが、コードをコンパクトにシリアル化するのには最適ではありません。
それは1回のパスで_verified_ SSAを生成することができます- @titzerによってコメント「速い有効にすることで、WebAssemblyの制御フロー構成を簡素化、消費者のデザイン、シンプルな検証、SSA形式に簡単に、1つのパス変換...」@oridb。 wasmがエンコーディングにSSAを使用したとしても、それを検証し、wasm制御フローの制限で簡単にドミネーター構造を計算するという負担があります。
wasmのエンコード効率の多くは、定義がスタック順に使用される単一の用途を持つ一般的なコードパターン用に最適化されているためと思われます。 SSAエンコーディングでもそうできると思いますので、同様のエンコーディング効率になる可能性があります。 ダイヤモンドパターンのif_else
などの演算子も大いに役立ちます。 しかし、wasm構造がないと、すべての基本ブロックがレジスタから定義を読み取り、結果をレジスタに書き込む必要があるように見えます。これはそれほど効率的ではない可能性があります。 たとえば、wasmは、スコープされたスタック値をスタックの上位および基本ブロックの境界を越えて参照できるpick
演算子を使用するとさらにうまくいくと思います。
wasmは、ほとんどのコードをSSAスタイルでエンコードできることからそれほど遠くないと思います。 定義が基本ブロック出力としてスコープツリーに渡された場合、それは完全である可能性があります。 SSAエンコーディングはCFGの問題に直交している可能性があります。 たとえば、wasm CFG制限のあるSSAエンコーディングが存在する可能性があり、CFG制限のあるレジスタベースのVMが存在する可能性があります。
wasmの目標は、最適化の負担をランタイムコンシューマーから排除することです。 攻撃対象領域が増えるため、ランタイムコンパイラに複雑さを加えることには強い抵抗があります。 設計上の課題の多くは、パフォーマンスを損なうことなくランタイムコンパイラを簡素化するために何ができるかを尋ねることであり、多くの議論があります。
さて、今ではおそらく手遅れですが、リルーパーアルゴリズムまたはその変形がすべての場合に「十分に良い」結果を生み出すことができるという考えに疑問を投げかけたいと思います。 ほとんどの場合、ほとんどのソースコードには最初から還元不可能な制御フローが含まれていないため、最適化によって問題が発生することはほとんどありません。たとえば、重複するブロックのマージの一部として含まれている場合は、おそらく教えることができます。しないでください。 しかし、病理学的症例はどうですか? たとえば、コンパイラがこの疑似Cのような構造を持つ通常の関数に変換したコルーチンがある場合はどうなりますか?
void transformed_coroutine(struct autogenerated_context_struct *ctx) {
int arg1, arg2; // function args
int var1, var2, var3, …; // all vars used by the function
switch (ctx->current_label) { // restore state
case 0:
// initial state, load function args caller supplied and proceed to start
arg1 = ctx->arg1;
arg2 = ctx->arg2;
break;
case 1:
// restore all vars which are live at label 1, then jump there
var2 = ctx->var2;
var3 = ctx->var3;
goto resume_1;
[more cases…]
}
[main body goes here...]
[somewhere deep in nested control flow:]
// originally a yield/await/etc.
ctx->var2 = var2;
ctx->var3 = var3;
ctx->current_label = 1;
return;
resume_1:
// continue on
}
したがって、ほとんどの場合、通常の制御フローがありますが、その途中にいくつかのゴトが向けられています。 これは、 LLVMコルーチンが大まかに
「通常の」制御フローが十分に複雑な場合、そのようなものを再ループする良い方法はないと思います。 (間違っているかもしれません。)あなたが潜在的にすべての降伏点のための別々のコピーを必要とする、機能の巨大な部分を複製、またはあなたが巨大なスイッチに全部を回すのどちらか、@kripkenによると、典型的なコードに遅くrelooperよりも4倍です(それ自体は、おそらくリルーパーをまったく必要としないよりもいくらか遅いです)。
VMは、ジャンプスレッドの最適化によって巨大なスイッチのオーバーヘッドを削減できますが、明示的なgotoを受け入れるよりも、VMがこれらの最適化を実行し、コードがどのようにgotosに還元されるかを推測する方が、確かにコストがかかります。 @kripkenが言うように、それはまたあまり予測できません。
おそらく、この種の変換を行うことは、最初は悪い考えです。その後、SSAベースの最適化では何も支配されないため、アセンブリレベルで行う方がよいかもしれません。代わりに、wasmは最終的にネイティブコルーチンサポートを取得する必要がありますか? しかし、コンパイラーは変換を実行する前にほとんどの最適化を実行でき、少なくともLLVMコルーチンの設計者は、コード生成まで変換を遅らせる緊急の必要性を認識していなかったようです。 一方、ポータブルバイトコード(コンパイラ)、VMに変換を実行させるよりも、変換済みのコードを適切にサポートする方が柔軟性があります。
とにかく、コルーチンはほんの一例です。 私が考えることができるもう1つの例は、VM内にVMを実装することです。 JITのより一般的な機能は、gotoを必要としないサイド出口ですが、サイドエントリを必要とする状況もあります。これも、ループの途中でgotoを必要とする場合などです。 もう1つは、最適化されたインタープリターです。wasmをターゲットとするインタープリターが、ネイティブコードをターゲットとするインタープリターと実際に一致するわけではありません。これにより、少なくとも計算されたgotoのパフォーマンスが向上し、アセンブリに深く入り込むことができます。ただし、計算されたgotoの動機の一部は、各ケースに独自のジャンプ命令を与えることで分岐予測を行うため、各オペコードハンドラーの後に個別のスイッチを設定することで、効果の一部を再現できる可能性があります。 または、少なくとも、現在の指示の後に一般的に来る特定の指示を確認するためのifまたは2を用意します。 そのパターンには、構造化された制御フローで表現できる特殊なケースがいくつかありますが、一般的なケースではありません。 等々…
確かに、VMに多くの作業を行わせることなく、任意の制御フローを許可する方法があります。 ストローマンの考えは壊れているかもしれません。子スコープへのジャンプが許可されるスキームを使用できますが、入力する必要のあるスコープの数がターゲットブロックで定義された制限より少ない場合に限ります。 制限はデフォルトで0(親スコープからのジャンプなし)になり、現在のセマンティクスが保持されます。ブロックの制限は、親ブロックの制限+ 1(確認が容易)を超えることはできません。 また、VMは、その優位性ヒューリスティックを「Yの親である場合はXがYを支配する」から「Yの子ジャンプ制限よりも距離が大きいYの親である場合はXがYを支配する」に変更します。 (これは控えめな概算であり、正確なドミネーターセットを表すことは保証されていませんが、既存のヒューリスティックにも同じことが当てはまります。内側のブロックが外側のブロックの下半分を支配する可能性があります。)還元不可能な制御フローを持つコードのみ制限を指定する必要がありますが、一般的なケースではコードサイズは増加しません。
編集:興味深いことに、それは基本的にブロック構造を支配ツリーの表現にします。 それを直接表現する方がはるかに簡単だと思います。基本ブロックのツリー。ブロックは兄弟、祖先、または直接の子ブロックにジャンプできますが、それ以上の子孫にはジャンプできません。 「ブロック」がサブループを間に挟んだ複数の基本ブロックで構成されている既存のスコープ構造に、それがどのように最適にマッピングされるかはわかりません。
FWIW:Wasmには特定のデザインがあります。これは、「ネストの制限により、ループの外側からループの中央に分岐できないことを除いて」、非常に重要な言葉で説明されています。
それが単なるDAGの場合、検証では分岐が順方向に進んでいることを確認できますが、ループを使用すると、ループの外側からループの中央に分岐できるため、ネストされたブロックデザインになります。
CFGはこの設計の一部であり、もう1つはデータフローです。値のスタックがあり、値のスタックを巻き戻すためにブロックを編成することもできます。これにより、ライブ範囲をコンシューマーに非常に便利に伝達できるため、SSAへの変換作業を節約できます。 。
wasmをSSAエンコーディングに拡張することが可能です( pick
追加し、ブロックが複数の値を返すことを許可し、ループエントリにポップ値を持たせる)ので、興味深いことに、効率的なSSAデコードに必要な制約は必要ないかもしれません(すでにSSAでエンコードされている可能性があります)! これは関数型言語につながります(効率のためにスタックスタイルのエンコーディングがあるかもしれません)。
これが任意のCFGを処理するように拡張された場合、次のようになります。 これはSSAスタイルのエンコーディングであるため、値は定数です。 それでもスタックスタイルにかなり適合しているようですが、すべての詳細がはっきりしているわけではありません。 したがって、 blocks
内で、そのセット内の他のラベル付きブロック、または制御を別のブロックに転送するために使用される他の規則に分岐することができます。 ブロック内のコードは、スタックの上位にある値スタックの値を参照して、すべてを渡す手間を省くことができます。
(func f1 (arg1)
(let ((c1 10)) ; Some values up the stack.
(blocks ((b1 (a1 a2 a3)
... (br b3)
(br b2 (+ a1 a2 a3 arg1 c1)))
(b2 (a1)
... (br b1 ...))
(b3 ()
...))
.. regular structured wasm ..
(br b2 ...)
....
(br b3)
...
))
しかし、Webブラウザはこれを内部で効率的に処理するでしょうか?
スタックマシンのバックグラウンドを持つ誰かがコードパターンを認識し、それをスタックエンコーディングに一致させることができますか?
ここに既約ループに関するいくつかの興味深い議論がありますhttp://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf
クイックパスですべてを追跡したわけではありませんが、エントリノードを追加して既約ループを既約ループに変換することについて言及しています。 wasmの場合、現在のソリューションと同様ですが、このために定義された変数を使用して、ループ内でディスパッチするための定義済みの入力をループに追加するように聞こえます。 上記は、これが処理中に仮想化され、最適化されていることを示しています。 おそらく、このようなものがオプションになる可能性がありますか?
これが間近に迫っていて、プロデューサーがすでに同様の手法を使用する必要があるが、ローカル変数を使用する必要がある場合、wasmがより高度なランタイムでより高速に実行される可能性があるため、今すぐ検討する価値がありますか? これはまた、これを探索するためのランタイム間の競争のインセンティブを生み出す可能性があります。
これは正確には任意のラベルやgotoではありませんが、これらが変換される可能性のあるものは、将来効率的にコンパイルされる可能性があります。
ちなみに、私はこの問題について@oridbと@comexと強く協力しています。
これは手遅れになる前に対処すべき重要な問題だと思います。
WebAssemblyの性質を考えると、今あなたが犯した間違いは、今後数十年続く可能性があります(Javascriptを見てください!)。 そのため、この問題は非常に重要です。 何らかの理由で今すぐgotosをサポートすることは避けてください(たとえば、最適化を容易にするために、これは---率直に言って---一般的なものに対する特定の実装の影響です。正直なところ、それは怠惰だと思います)。長期的には問題。
将来の(または現在の、しかし将来の)WebAssembly実装は、ラベルを適切に処理するために、通常のwhile / switchパターンを特殊なケースで認識してラベルを実装しようとしていることがわかります。 それはハックです。
WebAssemblyは白紙の状態なので、今こそダーティハック(というよりは、それらの要件)を回避するときです。
@darkuranium :
現在指定されているWebAssemblyは、ブラウザーとツールチェーンですでに出荷されており、開発者は、その設計でレイアウトされた形式をとるコードを既に作成しています。 したがって、デザインを壊すような方法で変更することはできません。
ただし、下位互換性のある方法で設計に追加することはできます。 関係者の誰もがgoto
が役に立たないと思っているとは思いません。 構文的なおもちゃのマナーだけでなく、私たち全員が定期的にgoto
使用しているのではないかと思います。
この時点で、やる気のある人は、意味のある提案を考え出し、それを実行する必要があります。 確かなデータが得られれば、そのような提案が却下されることはないと思います。
WebAssemblyの性質を考えると、今あなたが犯した間違いは、今後数十年続く可能性があります(Javascriptを見てください!)。 そのため、この問題は非常に重要です。 何らかの理由で今すぐgotosをサポートすることは避けてください(たとえば、最適化を容易にするために、これは---率直に言って---一般的なものに対する特定の実装の影響です。正直なところ、それは怠惰だと思います)。長期的には問題。
私はあなたのはったりを呼び出しますので:私はあなたが表示さモチベーションを持って、上のIの詳細として提案し、実装を考え出すていないと思いますが、率直に言って怠惰です。
もちろん生意気です。 スレッド、GC、SIMDなど、その機能が最も重要である理由について情熱的で賢明な議論をしている人々がいることを考えてみてください。これらの問題の1つに取り組むのを手伝っていただければ幸いです。 私が言及する他の機能のためにそうしている人々がいます。 これまでのところ、 goto
ありません。 このグループの貢献ガイドラインをよく理解し、楽しみに参加
そうでなければ、 goto
は素晴らしい将来の機能だと思い
こんにちは。 私はWebAssemblyからIRに、そしてWebAssemblyに戻る翻訳を書いている最中です。そして、このテーマについて人々と話し合いました。
還元不可能な制御フローをWebAssemblyで表現するのは難しいと指摘されています。 既約の制御フローをときどき書き出すコンパイラを最適化するのは面倒であることがわかります。 これは、複数のエントリポイントを持つ以下のループのようなものである可能性があります。
if (x) goto inside_loop;
// banana
while(y) {
// things
inside_loop:
// do things
}
EBBコンパイラーは以下を生成します:
entry:
cjump x, inside_loop
// banana
jump loop
loop:
cjump y, exit
// things
jump inside_loop
inside_loop:
// do things
jump loop
exit:
return
次に、これをWebAssemblyに変換します。 問題は、逆コンパイラーは何年も把握してい
翻訳される前に、コンパイラーはこれに対してトリックを実行します。 しかし、最終的には、コードをスキャンして、構造の始まりと終わりを配置することができます。 フォールスルージャンプを排除すると、次の候補になります。
<inside_loop, if(x)>
// banana
<loop °>
<exit if(y)>
// things
</inside_loop, if(x)>
// do things
</loop ↑>
</exit>
次に、これらからスタックを構築する必要があります。 どちらが一番下に行きますか? これは「ループ内」または「ループ」のいずれかです。 これはできないので、スタックを切り取ってコピーする必要があります。
if
// do things
else
// banana
end
loop
br out
// things
// do things
end
これで、これをWebAssemblyに変換できます。 申し訳ありませんが、これらのループがどのように構成されているかはまだよくわかりません。
古いソフトウェアを考えれば、これは特に問題ではありません。 新しいソフトウェアはWebアセンブリに変換される可能性があります。 しかし、問題はコンパイラーの動作にあります。 彼らは_decades_の基本ブロックで制御フローを実行しており、すべてがうまくいくと想定しています。
技術的には、言語は翻訳されてから翻訳されます。 ドラマなしで価値が境界を越えてきれいに流れることを可能にするメカニズムだけが必要です。 構造化されたフローは、コードを読むことを意図している人にのみ役立ちます。
ただし、たとえば、次の場合も同様に機能します。
cjump x, label(1)
// banana
0: label
cjump y, label(2)
// things
1: label
// do things
jump label(0)
2: label
// exit as usual, picking the values from the top of the stack.
数値は暗黙的です。つまり、コンパイラが「ラベル」を検出すると、コンパイラは新しい拡張ブロックを開始し、0からインクリメントし始める新しいインデックス番号を与えることを認識します。
静的スタックを生成するために、ラベルへのジャンプに遭遇したときにスタック内にあるアイテムの数を追跡できます。 ラベルにジャンプした後、スタックに一貫性がなくなる場合、プログラムは無効です。
上記が悪い場合は、各ラベルに明示的なスタック長(絶対値が圧縮に悪い場合は、最後にインデックス付けされたラベルのスタックサイズからのデルタ)を追加し、各ラベルにマーカーを追加して、値の数をジャンプすることもできます。ジャンプ中にスタックの一番上からコピーします。
制御フローをどのように表現するかによって、gzipの裏をかくことはできないので、ここで最も苦労している人に適したフローを選択することができます。 (「gzipをアウトスマートにする」ための柔軟なコンパイラツールチェーンを使用して説明できます。必要に応じて、メッセージを送信してデモを作成してください!)
私は今、粉々になったような気がします。 WebAssemblyの仕様を読み直して、既約の制御フローがMVPから意図的に除外されていることを確認しました。これは、おそらくemscriptenが初期の問題を解決しなければならなかったためです。
WebAssemblyで既約制御フローを処理する方法の解決策は、論文「Emscripten:LLVMからJavaScriptへのコンパイラ」で説明されています。 relooperは、プログラムを次のように再編成します。
_b_ = bool(x)
_b_ == 0 if
// banana
end
block loop
_b_ if
// do things
_b_ = 0
else
y br_if 2
// things
_b_ = 1
end
br 0
end end
理由は、構造化された制御フローがソースコードダンプの読み取りに役立つということでした。これは、ポリフィルの実装に役立つと考えられています。
WebAssemblyからコンパイルする人々は、おそらく、折りたたまれた制御フローを処理および分離するように適応します。
そう:
loop
ブロックが自然なループであることを引き続き保証する必要があります。block
:end
続くBBは、ブロックを開始するBBによって支配されますが、 end
前のBBによって支配されません( br
出た場合はスキップされるため) )。loop
:end
後のBBは、 end
前のBBによって支配されます( end
実行しない限り、 end
後の命令に到達できないため)。if
:end
後のBBはすべて、 if
前のBBによって支配されます。br
、 return
、 unreachable
:br
、 return
、またはunreachable
直後のBBには到達できません。)br_if
、 br_table
:br_if
/ br_table
前のBBが後のBBを支配します。goto
命令がどのように機能するかを考えてください。 AがBを支配し、BがCを支配するとします。if
命令を使用し、複数ある場合はbr_table
を使用します)。これに基づいて、ストローマンの提案があります。
loop
、 block
、またはlabels
)と、対応するものまでのすべてを意味します。 end
、または単一の非ブロック命令(スタックに影響を与えてはなりません)。labels
は、他のブロックタイプの命令のように単一のラベルを作成する代わりに、N + 1個のラベルを作成します。NはN個の子を指し、1つはlabels
ブロックの終わりを指します。 それぞれの子で、ラベルインデックス0からN-1は順番に子を参照し、ラベルインデックスNは終了を参照します。言い換えれば、あなたが持っている場合
loop ;; outer
labels 3
block ;; child 0
br X
end
nop ;; child 1
nop ;; child 2
end
end
Xに応じて、 br
は次のことを指します。
| X | ターゲット|
| ---------- | ------ |
| 0 | block
終わり|
| 1 | 子0( block
先頭)|
| 2 | 子供1(nop)|
| 3 | 子供2(nop)|
| 4 | labels
終わり|
| 5 | アウターループの始まり|
実行は最初の子から始まります。
実行が子の1つの終わりに達すると、次の子に進みます。 最後の子の終わりに達すると、最初の子に戻ります。 (これは対称性のためです。子の順序は重要ではないためです。)
子の1つに分岐すると、 labels
開始時にオペランドスタックがその深さまで巻き戻されます。
最後まで分岐することも同様ですが、 block
と同様に、最後まで分岐するとオペランドがポップされ、巻き戻し後にプッシュされます。
ドミナンス: labels
命令の前の基本ブロックが各子を支配し、 labels
終了後のBBも支配します。 子供たちはお互いを支配したり、終わりを支配したりしません。
デザインノート:
Nは事前に指定されているため、コードを1回のパスで検証できます。 labels
ブロックの最後に到達して、その中のインデックスのターゲットを知る前に、子の数を知る必要があるのは奇妙なことです。
ラベル間でオペランドスタックに値を渡す方法が最終的にあるかどうかはわかりませんが、 block
またはloop
に値を渡すことができないことと同様に、開始がサポートされていない可能性がありますと。
でも、ループに飛び込むことができたら本当にいいですね。 IIUC、その場合が考慮された場合、厄介なloop + br_tableコンボは必要ありません...
編集:ああ、 labels
上にジャンプすることで、 loop
なしでループを作成できます。 私がそれを逃したなんて信じられない。
@qwertie特定のループが自然なループでない場合、wasm-targetingコンパイラはlabels
代わりにloop
labels
を使用してそれを表現する必要があります。 それがあなたが言及しているものであるならば、制御フローを表現するためにスイッチを追加する必要は決してないはずです。 (結局のところ、最悪の場合、関数内のすべての基本ブロックにラベルが付いた1つの巨大なlabels
ブロックを使用できます。これは、コンパイラーにドミナンスと自然ループについて通知しないため、見逃す可能性があります。最適化。ただし、 labels
は、これらの最適化が適用できない場合にのみ必要です。)
ネストされたループ構造は、還元性が保証するものであり、最初はほとんど捨てられています。 [...] JavaScriptCore、V8、およびSpiderMonkeyの現在のWebAssembly実装を確認しましたが、これらはすべてこのパターンに従っているようです。
完全ではありません。少なくともSMでは、IRグラフは完全に一般的なグラフではありません。 構造化されたソース(JSまたはwasm)から生成された後の特定のグラフ不変量を想定し、多くの場合、アルゴリズムを単純化および/または最適化します。 完全に一般的なCFGをサポートするには、パイプライン内のパスの多くを監査/変更して、これらの不変条件を想定しないようにするか(還元できない場合はパスを一般化または悲観化することにより)、グラフを還元可能にするためにノード分割の複製を事前に行う必要があります。 もちろん、これは確かに実行可能ですが、これが単に人工的なボトルネックであるという問題であるというのは真実ではありません。
また、多くのオプションがあり、エンジンが異なれば異なることを行うという事実は、プロデューサーが還元不可能性に前もって対処することで、還元不可能な制御フローが存在する場合に、いくらか予測可能なパフォーマンスが得られることを示唆しています。
過去に任意のgotoサポートを使用してwasmを拡張するための下位互換性のあるパスについて説明したとき、1つの大きな問題は、ここでのユースケースは何であるかということです。 「実際に還元不可能な制御フローのためのより効率的なcodegenを許可する」? 前者の場合は、任意のラベル/ gotoを埋め込むスキームが必要になると思います(下位互換性があり、将来のブロック構造のtry / catchで構成されます)。 それは、コスト/利益と上記の問題を比較検討するだけの問題です。
しかし、後者のユースケースの場合、私たちが観察したことの1つは、Duffのデバイスケースが実際に見られることです(これは実際にはループを展開する効率的な方法ではありません...)。パフォーマンスが重要な場所で還元不可能性がポップアップするのは、インタープリターループです。 インタープリターループは、計算されたgotoを必要とする間接スレッドの恩恵も受けます。 また、強力なオフラインコンパイラでも、インタプリタループは最悪のレジスタ割り当てを取得する傾向があります。 インタープリターループのパフォーマンスは非常に重要である可能性があるため、1つの質問は、エンジンが間接スレッドを実行して適切なregallocを実行できるようにする制御フロープリミティブが本当に必要かどうかです。 (これは私にとって未解決の質問です。)
@lukewagner
どのパスが不変条件に依存しているかについて、より詳細に聞きたいです。 私が提案した設計では、既約フロー用に別の構成を使用して、LICMのような最適化パスがそのフローを回避するのを比較的簡単にする必要があります。 しかし、私が考えていない他の種類の破損がある場合は、それらの性質をよりよく理解して、それらを回避できるかどうか、およびどのように回避できるかについてより良いアイデアを得ることができるようにしたいと思います。
過去に任意のgotoサポートを使用してwasmを拡張するための下位互換性のあるパスについて説明したとき、1つの大きな問題は、ここでのユースケースは何であるかということです。 「実際に還元不可能な制御フローのためのより効率的なcodegenを許可する」?
私にとっては後者です。 私の提案では、プロデューサーがリルーパータイプのアルゴリズムを実行して、バックエンドがドミネーターと自然ループを識別する作業を節約し、必要な場合にのみlabels
にフォールバックすることを期待しています。 ただし、これでもプロデューサーは簡単になります。 還元不可能な制御フローに大きなペナルティがある場合、理想的なプロデューサーは、ヒューリスティックを使用してコードを複製する方が効率的かどうか、機能する複製の最小量などを判断して、それを回避するために非常に懸命に取り組む必要があります。アップループの最適化では、これは実際には必要ありません。少なくとも、通常のマシンコードバックエンド(独自のループ最適化があります)の場合よりも必要ありません。
私は本当に、一般的な還元不可能な制御フローが実際にどのように行われているかについて、より多くのデータを収集する必要があります…
しかし、私の信念は、そのような流れにペナルティを課すことは本質的に恣意的であり、不必要であるということです。 ほとんどの場合、プログラムの実行時間全体への影響は小さいはずです。 ただし、ホットスポットに還元不可能な制御フローが含まれている場合は、重大なペナルティが発生します。 将来的には、WebAssembly最適化ガイドにこれが一般的な落とし穴として含まれ、それを特定して回避する方法が説明される可能性があります。 私の信念が正しければ、これはプログラマーにとってまったく不要な形の認知的オーバーヘッドです。 また、オーバーヘッドが小さい場合でも、WebAssemblyには、ネイティブコードと比較してすでに十分なオーバーヘッドがあるため、余分なものを回避する必要があります。
私は自分の信念が間違っていると説得することを受け入れています。
インタープリターループのパフォーマンスは非常に重要である可能性があるため、1つの質問は、エンジンが間接スレッドを実行して適切なregallocを実行できるようにする制御フロープリミティブが本当に必要かどうかです。
面白そうに聞こえますが、もっと汎用的なプリミティブから始めたほうがいいと思います。 結局のところ、通訳者向けに調整されたプリミティブは、既約の制御フローを処理するためにバックエンドを必要とします。 あなたがその弾丸を噛むつもりなら、一般的なケースもサポートするかもしれません。
あるいは、私の提案はすでに通訳者にとってまともな原始的なものとして役立つかもしれません。 labels
とbr_table
を組み合わせると、関数内の任意のポイントにジャンプテーブルを直接ポイントできるようになります。これは、計算されたgotoとそれほど変わりません。 (少なくとも最初は制御フローをスイッチブロック内のポイントに向けるCスイッチとは対照的に、ケースがすべてゴトスである場合、コンパイラは余分なジャンプを最適化できるはずですが、複数の「冗長」を合体させる可能性もありますステートメントを1つに切り替えて、各命令ハンドラーの後に個別にジャンプする利点を台無しにします。)レジスタ割り当ての問題が何であるかはわかりませんが...
@comex既約制御フローが存在する場合、関数レベルで最適化パス全体をオフにするだけで
>>
ネストされたループ構造は、還元性が保証するものです。
初めはほとんど捨てられていました。 [...]現在を確認しました
JavaScriptCore、V8、SpiderMonkeyでのWebAssemblyの実装、および
それらはすべてこのパターンに従っているようです。完全ではありません。少なくともSMでは、IRグラフは完全に一般的なグラフではありません。 私たち
から生成された後に続く特定のグラフ不変量を想定します
構造化されたソース(JSまたはwasm)であり、多くの場合、
アルゴリズム。V8でも同じです。 それは実際、両方のSSAに対する私の大きな不満の1つです。
彼らがほとんど定義しないそれぞれの文献と実装
「整形式」のCFGを構成するものですが、暗黙的にさまざまなものを想定する傾向があります
とにかく文書化されていない制約、通常は
言語フロントエンド。 私は、既存のコンパイラの多くの/ほとんどの最適化に賭けます
真に任意のCFGを処理することはできません。
@lukewagnerが言うように、既約制御の主なユースケースはおそらく
最適化されたインタプリタのための「スレッドコード」。 それらがどれほど関連性があるかを言うのは難しい
Wasmドメイン用であり、その不在が実際に最大であるかどうか
ボトルネック。
還元不可能な制御フローについて多くの人々と話し合った
コンパイラIRを調査する場合、「最もクリーンな」ソリューションはおそらく追加することです
相互再帰ブロックの概念。 それはたまたまWasmに合うでしょう
制御構造は非常にうまく機能します。
LLVMでのループ最適化は、通常、還元不可能な制御フローを無視し、最適化を試みません。 それらが基づいているループ分析は自然なループのみを認識するため、ループとして認識されないCFGサイクルが存在する可能性があることに注意する必要があります。 もちろん、他の最適化は本質的にローカルであり、既約CFGで問題なく機能します。
メモリから、そしておそらく間違って、SPEC2006は401.bzip2に単一の既約ループを持っています、そしてそれはそれだけです。 実際には非常にまれです。
Clangは、計算されたgotoを使用する関数で単一のindirectbr
命令のみを発行します。 これには、スレッド化されたインタープリターを、ループヘッダーとしてindirectbr
ブロックを使用する自然なループに変える効果があります。 LLVM IRを離れた後、単一のindirectbr
がコードジェネレーターでテール複製され、元のもつれが再構築されます。
既約制御フローのシングルパス検証アルゴリズムはありません
私が知っていること。 還元可能な制御フローのみの設計上の選択は
この要件の影響を強く受けます。
前述のように、還元不可能な制御フローは少なくとも2つモデル化できます
違う方法。 switchステートメントを含むループは実際に最適化できます
単純なローカルジャンプスレッディングによって元の既約グラフに変換します
最適化(例えば、定数の割り当てでパターンを折りたたむことによる)
ローカル変数への分岐が発生し、次に条件分岐への分岐が発生します。
すぐにそのローカル変数をオンにします)。
したがって、既約制御構造はまったく必要ありません。
を回復するための単一のコンパイラバックエンド変換の問題のみ
元の既約グラフとそれを最適化する(コンパイラーが
還元不可能な制御フローをサポートします。これは、4つのブラウザのいずれもサポートしていません。
私の知る限り)。
一番、
-ベン
2017年4月20日木曜日午前5時20分、Jakob Stoklund Olesen <
[email protected]>は次のように書いています:
LLVMでのループ最適化は、通常、還元不可能な制御フローを無視します
最適化しようとしないでください。 彼らが基づいているループ分析は
自然なループのみを認識するので、次のことができることに注意する必要があります
ループとして認識されないCFGサイクルである。 もちろん、他
最適化は本質的により局所的であり、既約でうまく機能します
CFG。メモリから、そしておそらく間違っている、SPEC2006には単一の既約ループがあります
401.bzip2とそれだけです。 実際には非常にまれです。Clangは、を使用する関数で1つのindirectbr命令のみを発行します。
計算されたgoto。 これは、スレッド通訳者をに変える効果があります
ループヘッダーとしてindirectbrブロックを使用した自然ループ。 去った後
LLVM IR、単一の間接表現はコードジェネレーターでテール複製されます
元のもつれを再構築します。—
あなたが言及されたので、あなたはこれを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 、
またはスレッドをミュートします
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
。
さらに、既約構造が追加された場合、
WebAssembly、TurboFan(V8の最適化JIT)では機能しないため、
関数は最終的に解釈される(非常に遅い)か、
ベースラインコンパイラによってコンパイルされます(やや遅い)。
還元不可能な制御フローをサポートするためにターボファンをアップグレードすることに努力を投資します。
つまり、WebAssemblyで既約の制御フローを持つ関数は
おそらくパフォーマンスが大幅に低下することになります。
もちろん、別のオプションでは、V8のWebAssemblyエンジンで
ターボファンの還元可能なグラフをフィードするためのリルーパーですが、それはコンパイルになります
(そしてスタートアップはもっと悪い)。 再ループは私の中でオフラインの手順のままにする必要があります
そうでなければ、避けられないエンジンコストになってしまいます。
一番、
-ベン
2017年5月1日月曜日の午後12時48分、Ben L. [email protected]は次のように書いています。
既約制御のためのシングルパス検証アルゴリズムはありません
私が知っている流れ。 還元可能な制御フローのみの設計選択
この要件の影響を強く受けました。前述のように、還元不可能な制御フローは少なくとも2つモデル化できます
違う方法。 switchステートメントを含むループは実際に最適化できます
単純なローカルジャンプスレッディングによって元の既約グラフに変換します
最適化(例えば、定数の割り当てでパターンを折りたたむことによる)
ローカル変数への分岐が発生し、次に条件分岐への分岐が発生します。
すぐにそのローカル変数をオンにします)。したがって、既約制御構造はまったく必要ありません。
を回復するための単一のコンパイラバックエンド変換の問題のみ
元の既約グラフとそれを最適化する(コンパイラーが
還元不可能な制御フローをサポートします。これは、4つのブラウザのいずれもサポートしていません。
私の知る限り)。一番、
-ベン2017年4月20日木曜日午前5時20分、Jakob Stoklund Olesen <
[email protected]>は次のように書いています:LLVMでのループ最適化は、通常、還元不可能な制御フローを無視します
最適化しようとしないでください。 彼らが基づいているループ分析は
自然なループのみを認識するので、次のことができることに注意する必要があります
ループとして認識されないCFGサイクルである。 もちろん、他
最適化は本質的により局所的であり、既約でうまく機能します
CFG。メモリから、そしておそらく間違っている、SPEC2006には単一の既約ループがあります
401.bzip2で、それだけです。 実際には非常にまれです。Clangは、を使用する関数で1つのindirectbr命令のみを発行します。
計算されたgoto。 これは、スレッド通訳者をに変える効果があります
ループヘッダーとしてindirectbrブロックを使用した自然ループ。 去った後
LLVM IR、単一の間接表現はコードジェネレーターでテール複製されます
元のもつれを再構築します。—
あなたが言及されたので、あなたはこれを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 、
またはスレッドをミュートします
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
。
既約制御フローの線形時間検証のための確立された方法があります。 注目すべき例はJVMです。スタックマップを使用すると、線形時間検証が行われます。 WebAssemblyには、すべてのブロックのような構成にすでにブロック署名があります。 複数の制御フローパスがマージされるすべてのポイントで明示的な型情報を使用するため、固定小数点アルゴリズムを使用する必要はありません。
(余談ですが、少し前に、架空のpick
演算子がブロックの外側を任意の深さで読み取ることを禁止する理由を尋ねました。これは1つの答えです。署名が拡張されてすべてをpick
と記述しない限り、 pick
タイプチェックには、より多くの情報が必要になります。)
もちろん、スイッチ付きループのパターンはジャンプスレッド化できますが、信頼するのは現実的ではありません。 エンジンがそれを最適化しない場合、それは破壊的なレベルのオーバーヘッドを持ちます。 ほとんどのエンジンがそれを最適化する場合、言語自体から還元不可能な制御フローを排除することによって何が達成されるかは不明です。
ため息…早く返事をするつもりだったのですが、人生が邪魔になりました。
私はいくつかのJSエンジンを調べてきましたが、還元不可能な制御フローが「機能している」という主張を弱める必要があると思います。 それを機能させるのはまだ難しいとは思いませんが、実際に利益をもたらす方法で適応させるのが難しい構造がいくつかあります…
さて、議論のために、最適化パイプラインが還元不可能な制御フローを適切にサポートするようにするのは難しすぎると仮定しましょう。 JSエンジンは、次のように、ハッキーな方法で簡単にサポートできます。
バックエンド内で、 labels
ブロックを、最後の最後までループ+スイッチであるかのように扱います。 つまり、 labels
ブロックが表示された場合、それを各ラベルを指す外向きのエッジを持つループヘッダーとして扱い、ラベルをターゲットとするbranch
が表示された場合、次のように作成します。実際のターゲットラベルではなく、 labels
ヘッダーを指すエッジ-どこかに個別に保存する必要があります。 実際のloop + switchが行う必要があるように、ターゲットラベルを格納するために実際の変数を作成する必要はありません。 分岐命令の一部のフィールドに値を格納するか、目的に応じて別の制御命令を作成するだけで十分です。 次に、最適化、スケジューリング、さらにはレジスタ割り当てでさえ、2つのジャンプがあるふりをすることができます。 しかし、実際にネイティブジャンプ命令を生成するときが来たら、そのフィールドをチェックして、ターゲットラベルに直接ジャンプを生成します。
たとえば、ブランチをマージ/削除する最適化に問題がある可能性がありますが、それを回避するのは非常に簡単です。 詳細はエンジンの設計によって異なります。
ある意味で、私の提案は@titzerの「単純なローカルジャンプスレッドの最適化」と同等です。 「ネイティブ」の還元不可能な制御フローをループ+スイッチのように見せることを提案していますが、別の方法として、実際のループ+スイッチを特定することもできます。つまり、@ titzerの「ローカル変数への定数の割り当てが発生するパターンでは、そのローカル変数をすぐにオンにする条件付き分岐への分岐」–そして、パイプラインの後半で間接分岐を削除できるようにするメタデータを追加します。 この最適化が遍在するようになれば、それは明示的な指示のまともな代替となる可能性があります。
いずれにせよ、ハッキーなアプローチの明らかな欠点は、最適化が実際の制御フローグラフを理解しないことです。 それらは、任意のラベルが他のラベルにジャンプできるかのように効果的に機能します。 特に、レジスタ割り当ては、たとえば、次の擬似コードのように、特定のラベルにジャンプする直前に常に割り当てられている場合でも、変数をすべてのラベルでライブとして処理する必要があります。
a:
control = 1;
goto x;
b:
control = 2;
goto x;
...
x:
// use control
これにより、場合によっては、レジスタの使用が大幅に最適化されなくなる可能性があります。 しかし、後で説明するように、JITが使用するライブネスアルゴリズムは、とにかく、基本的にこれをうまく行うことができない可能性があります…
いずれにせよ、まったく最適化しないよりも、遅く最適化する方がはるかに優れています。 単一の直接ジャンプは、ジャンプ+比較+ロード+間接ジャンプよりもはるかに優れています。 CPU分岐予測子は、最終的には過去の状態に基づいて後者のターゲットを予測できる可能性がありますが、コンパイラーほどではありません。 また、「現在の状態」変数にレジスタやメモリを費やすことを回避できます。
表現に関しては、明示的( labels
命令など)または暗黙的(特定のパターンに従った実際のループ+スイッチの最適化)のどちらが良いですか?
暗黙のメリット:
仕様をスリムに保ちます。
既存のループ+スイッチコードですでに動作する可能性があります。 しかし、binaryenが生成するものを調べて、それが十分に厳密なパターンに従っているかどうかを確認していません。
還元不可能な制御フローを表現する祝福された方法をハックのように感じさせることは、それが一般的に遅く、可能な場合は避けるべきであるという事実を浮き彫りにします。
暗黙の欠点:
それはハックのように感じます。 確かに、
「最適化の崖」を作成します。これは、WebAssemblyがJSと比較して一般的に回避することになっています。 振り返ってみると、最適化される基本的なパターンは、「ローカル変数への定数の割り当てが発生し、次にそのローカル変数をすぐにオンにする条件付きブランチへのブランチ」です。 しかし、たとえば、間に他の命令がある場合、または割り当てが実際にwasm const
命令を使用しておらず、最適化のために定数として知られているものを使用している場合はどうなりますか? 一部のエンジンは、このパターンとして認識される点で他のエンジンよりも寛大である可能性がありますが、それを利用するコードは(意図的かどうかにかかわらず)ブラウザー間で大幅に異なるパフォーマンスを示します。 より明示的なエンコーディングを使用すると、期待がより明確になります。
仮想的な後処理ステップでIRのようにwasmを使用するのが難しくなります。 wasm-targetingコンパイラが通常の方法で処理を行い、最終的にrelooperを実行して最終的にwasmを生成する前に、内部IRを使用してすべての最適化/変換を処理する場合、魔法の命令シーケンスの存在を気にする必要はありません。 しかし、プログラムがwasmコード自体で変換を実行したい場合は、それらのシーケンスを分割することを回避する必要があり、これは煩わしいことです。
とにかく、私はどちらの方法でもそれほど気にしません-暗黙のアプローチを決定する場合、主要なブラウザは実際に関連する最適化を実行することを約束します。
…
既約フローをネイティブにサポートするという質問に戻ると、障害とは何か、どの程度のメリットがあるかという問題に戻ります。これをサポートするために変更する必要がある最適化パスのIonMonkeyの具体例を次に示します。
AliasAnalysis.cpp:逆のポストオーダー(1回)でブロックを反復し、以前にエイリアシングの可能性があると見なされたストアのみを調べることにより、(InstructionReorderingで使用される)命令の順序依存関係を生成します。 これは、循環制御フローでは機能しません。 ただし、(明示的にマークされた)ループは特別に処理され、ループ内の命令を同じループ内の任意の場所にある後のストアと照合する2番目のパスがあります。
- >だから、マーキング、いくつかのループがあることがあるだろうlabels
ブロック。 この場合、分析が不正確すぎてループ内の制御フローを気にしないため、 labels
ブロック全体をループとしてマークすると(個々のラベルを特別にマークしなくても)「正常に機能する」と思います。
FlowAliasAnalysis.cpp:少し賢い代替アルゴリズム。 また、逆のポストオーダーでブロックを繰り返しますが、各ブロックに遭遇すると、バックエッジを考慮に入れるループヘッダーを除いて、各ブロックの計算された最後のストア情報(すでに計算されていると想定)をマージします。
->メシエは、(a)ループのバックエッジを除いて、個々の基本ブロックの先行ブロックが常にその前に表示され、(b)ループが1つのバックエッジしか持てないことを前提としているためです。 これを修正する方法はいくつかありますが、おそらくlabels
明示的に処理する必要があり、アルゴリズムを線形に保つには、通常のAliasAnalysis
ように、かなり大雑把に機能する必要があります。
BacktrackingAllocator.cpp:レジスタ割り当ての同様の動作:命令のリストを線形逆パスし、ループのバックエッジに遭遇した場合を除いて、命令のすべての使用がその定義の後に表示される(つまり、前に処理される)と想定します。ループの開始時にライブを維持するだけで、ループ全体をライブで維持できます。
->すべてのラベルはループヘッダーのように扱う必要がありますが、活気はラベルブロック全体に拡張する必要があります。 実装するのは難しいことではありませんが、繰り返しになりますが、結果はハッキーなアプローチよりも優れているとは言えません。 私が思うに。
@comexここで
IonのBacktrackingAllocator.cppライブネスアルゴリズムにはある程度の作業が必要ですが、それは法外なことではありません。 OSRはループに複数のエントリを作成できるため、ほとんどのIonはすでにさまざまな形式の既約制御フローを処理しています。
ここでのより広い質問は、WebAssemblyエンジンが行うと予想される最適化です。 WebAssemblyがアセンブリのようなプラットフォームであり、プロデューサー/ライブラリがほとんどの最適化を行う予測可能なパフォーマンスを期待する場合、エンジンは大きな複雑なアルゴリズムを必要としないため、還元不可能な制御フローはかなり低コストになります。 。 WebAssemblyがより高レベルのバイトコードであり、より高レベルの最適化を自動的に実行し、エンジンがより複雑であると予想される場合、余分な複雑さを回避するために、還元不可能な制御フローを言語から排除することがより価値があります。
ところで、この号で言及する価値があるのは、 Braun et alのオンザフライSSA構築アルゴリズムです。これは、シンプルで高速なオンザフライSSA構築アルゴリズムであり、還元不可能な制御フローをサポートします。
iOSでWebAssemblyをqemuバックエンドとして使用することに興味があります。WebKit(および動的リンカーですが、コード署名をチェックします)は、メモリを実行可能としてマークできる唯一のプログラムです。 Qemuのcodegenは、gotoステートメントがcodegenを実行する必要のあるプロセッサーの一部であると想定しているため、gotosを追加しないとWebAssemblyバックエンドはほとんど不可能になります。
@ tbodt -Binaryenのリルーパーを使用できますか? それでは、基本的にWasm-with-gotoを生成し、それをWasmの構造化制御フローに変換しましょう。
@eholkマシンコードをwasmに直接変換するよりもはるかに遅いようです。
@tbodt Binaryenを使用すると、途中でIRが追加されますが、それほど遅くなることはないはずです。コンパイル速度が最適化されていると思います。 また、オプションでBinaryenオプティマイザーを実行できるため、gotoなどの処理以外の利点もあります。これにより、qemuオプティマイザーが実行しないこと(wasm固有の処理)が実行される場合があります。
実際、必要に応じて、それについてあなたと協力したいと思います:)Qemuをwasmに移植することは非常に役立つと思います。
したがって、考え直してみると、gotosはあまり役に立ちません。 Qemuのcodegenは、基本ブロックが最初に実行されたときにそれらのコードを生成します。 ブロックがまだ生成されていないブロックにジャンプすると、ブロックが生成され、前のブロックにgotoで次のブロックにパッチが適用されます。 私の知る限り、動的なコードのロードと既存の関数のパッチ適用は、WebAssemblyで実行できるものではありません。
@kripkenコラボレーションに興味がありますが、あなたとチャットするのに最適な場所はどこですか?
既存の関数に直接パッチを適用することはできませんが、 call_indirect
とWebAssembly.Table
を使用してコードをjitすることができます。 生成されていない基本ブロックについては、JavaScriptを呼び出し、WebAssemblyモジュールとインスタンスを同期的に生成し、エクスポートされた関数を抽出して、テーブルのインデックスに書き込むことができます。 今後の呼び出しでは、生成された関数が使用されます。
ただし、まだ誰かがこれを試したかどうかはわかりません。そのため、多くの荒削りな部分がある可能性があります。
テールコールが実装されていれば、それはうまくいく可能性があります。 そうしないと、スタックがすぐにオーバーフローします。
もう1つの課題は、デフォルトのテーブルにスペースを割り当てることです。 アドレスをテーブルインデックスにどのようにマップしますか?
もう1つのオプションは、新しい基本ブロックごとにwasm関数を再生成することです。 これは、使用されたブロックの数に等しい再コンパイルの数を意味しますが、コンパイル後にコードをすばやく実行する唯一の方法(特に内部ループ)であり、完全である必要はありません。再コンパイルすると、既存のブロックごとにBinaryen IRを再利用し、新しいブロックにIRを追加して、すべてのブロックでrelooperを実行できます。
(しかし、qemuに関数全体を遅延ではなく事前にコンパイルさせることができるかもしれませんか?)
@tbodtは、Binaryenでこれを行うためのコラボレーションです。1つのオプションは、作業でリポジトリを作成することです(そして、そこで問題を使用できます)。別のオプションは、qemuのBinaryenで特定の問題を開くことです。
qemuには「関数」の概念がないため、qemuに一度に関数全体をコンパイルさせることはできません。
ブロックのキャッシュ全体を再コンパイルすることに関しては、それは長い時間がかかるかもしれないように聞こえます。 qemuの組み込みプロファイラーの使用方法を理解してから、binaryenで問題を開きます。
サイド質問。 私の見解では、WebAssemblyを対象とする言語は、効率的な相互再帰関数を提供できるはずです。 それらの有用性の描写については、以下をお読みください: //sharp-gamedev.blogspot.com/2011/08/forgotten-control-flow-construct.html
特に、 Cheeryによって表現されたニーズは、相互再帰関数によって対処されているようです。
末尾再帰の必要性は理解していますが、相互再帰機能は、基盤となる機械がgotoを提供する場合にのみ実装できるのではないかと思います。 もしそうなら、それ以外の場合はWebAssemblyをターゲットにするのに苦労するプログラミング言語がたくさんあるので、それは彼らを支持する正当な議論になります。 そうでない場合は、相互再帰関数をサポートするための最小限のメカニズムが、(末尾再帰とともに)必要なすべてです。
@ davidgrenier 、Wasmモジュールの関数はすべて相互再帰的です。 それらについて非効率的だと思うことを詳しく説明できますか? 末尾呼び出しの欠如などについて言及しているだけですか?
一般的な末尾呼び出しが来ています。 末尾再帰(相互またはその他)は、その特殊なケースになります。
私はそれらについて何も非効率的だと言っていませんでした。 相互再帰関数は、WebAssemblyを対象とする言語実装者が必要とするすべてのものを提供するため、それらがあれば、一般的なgotoは必要ありません。
Gotoは、ビジュアルプログラミングの図からコードを生成するのに非常に便利です。 おそらく現在、ビジュアルプログラミングはあまり人気がありませんが、将来的にはより多くの人々を獲得することができ、wasmはそれに備える必要があると思います。 ダイアグラムとgotoからのコード生成の詳細: http :
今後のGo1.11リリースでは、WebAssemblyが実験的にサポートされます。 これには、ゴルーチン、チャネルなどを含むGoのすべての機能の完全なサポートが含まれます。ただし、生成されたWebAssemblyのパフォーマンスは現在それほど良くありません。
これは主に、goto命令が欠落しているためです。 goto命令がなければ、すべての関数でトップレベルのループとジャンプテーブルを使用する必要がありました。 ゴルーチンを切り替えるときは、関数のさまざまなポイントで実行を再開できる必要があるため、リルーパーアルゴリズムを使用することはできません。 relooperはこれを支援できず、goto命令のみが支援できます。
WebAssemblyがGoのような言語をサポートできるようになったのは素晴らしいことです。 しかし、真にWebのアセンブリであるためには、WebAssemblyは他のアセンブリ言語と同等に強力である必要があります。 Goには、他の多くのプラットフォーム用に非常に効率的なアセンブリを出力できる高度なコンパイラがあります。 これが、主にWebAssemblyの制限であり、Goコンパイラの制限ではなく、このコンパイラを使用してWebの効率的なアセンブリを出力できないことを主張したい理由です。
ゴルーチンを切り替えるときは、関数のさまざまなポイントで実行を再開できる必要があるため、リルーパーアルゴリズムを使用することはできません。
明確にするために、通常のgotoでは十分ではありません。ユースケースには計算されたgotoが必要ですが、それは正しいですか?
パフォーマンスの面では、通常のgotoで十分だと思います。 基本ブロック間のジャンプはとにかく静的であり、ゴルーチンを切り替えるには、ブランチにgotoを含むbr_table
で十分なパフォーマンスが必要です。 ただし、出力サイズは別の問題です。
各関数で通常の制御フローがあるように聞こえますが、ゴルーチンを再開するときに、関数のエントリから「中央」の他の特定の場所にジャンプする機能も必要です。そのような場所はいくつありますか? それがすべての基本ブロックである場合、リルーパーはすべての命令が通過するトップレベルループを発行するように強制されますが、それがほんのわずかであれば、それは問題にはならないはずです。 (これは、実際にはemscriptenでのsetjmpサポートで発生します。LLVMの基本ブロック間に必要な追加のパスを作成し、relooperにそれを通常どおりに処理させます。)
他の関数へのすべての呼び出しはそのような場所であり、ほとんどの基本ブロックには少なくとも1つの呼び出し命令があります。 コールスタックを多かれ少なかれ巻き戻し、復元しています。
なるほど、ありがとう。 ええ、それが実用的であるためには、静的なgotoまたはコールスタック復元サポート(これも考慮されています)のいずれかが必要であることに同意します。
CPSスタイルで関数を呼び出したり、WASMでcall/cc
を実装したりすることは可能ですか?
@Heimdell 、何らかの形の区切られた継続(別名「スタックスイッチング」)のサポートがロードマップにあります。これは、ほとんどすべての興味深いコントロールの抽象化に十分なはずです。 ただし、Wasmコールスタックは、埋め込み機能へのリエントラントコールを含め、他の言語と任意に混在させることができるため、無制限の継続(つまり、フルコール/ cc)をサポートすることはできません。したがって、コピー可能または移動可能であるとは見なされません。
このスレッドを読むと、任意のラベルとgotoが機能になる前に大きなハードルがあるという印象を受けます。
_ *ただし、還元不可能な制御フローを処理するBraun etalのオンザフライSSA構築アルゴリズムなどの代替手段があるかもしれません_
それでも問題が解決しない場合は、末尾呼び出しが進んでいます。言語コンパイラにgotoに変換するように依頼する価値があるかもしれませんが、WebAssembly出力の前の最後のステップとして、「ラベルブロック」を関数に分割します。 gotoを末尾呼び出しに変換します。
スキームデザイナーのGuySteeleの1977年の論文、 Lambda:The Ultimate GOTOによると、変換が可能であり、末尾呼び出しのパフォーマンスがgotoに厳密に一致する必要があります。
考え?
それでも問題が解決しない場合は、末尾呼び出しが進んでいます。言語コンパイラにgotoに変換するように依頼する価値があるかもしれませんが、WebAssembly出力の前の最後のステップとして、「ラベルブロック」を関数に分割します。 gotoを末尾呼び出しに変換します。
これは基本的にすべてのコンパイラが行うことです。私が知っている人は誰も、型付きEBBのグラフのためだけに、JVMで非常に多くの問題を引き起こす種類のアンマネージドgotoを提唱していません。 LLVM、GCC、Craneliftなどはすべて、内部表現として(おそらく既約の)SSA形式のCFGを持ち、Wasmからネイティブまでのコンパイラーは同じ内部表現を持っているので、その情報をできるだけ多く保持したいと思います。その情報をできるだけ少なく再構築します。 ローカルはSSAではなくなったため損失が大きく、Wasmの制御フローは任意のCFGではなくなったため損失があります。 Wasmが無限レジスタのSSAレジスタマシンであり、きめ細かいレジスタの活性情報が埋め込まれているAFAIKは、codegenにはおそらく最適ですが、コードサイズは肥大化します。任意のCFGでモデル化された制御フローを備えたスタックマシンは、おそらく最良の中間点です。 。 レジスターマシンでのコードサイズについては間違っているかもしれませんが、効率的にエンコードできるかもしれません。
既約の制御フローについては、フロントエンドで既約である場合でも、wasmで既約である場合、relooper / stackifierの変換によって制御フローが還元可能になるのではなく、既約がランタイム値に依存するように変換されるだけです。 これにより、バックエンドの情報が少なくなり、より悪いコードが生成される可能性があります。現在、既約CFGに適したコードを生成する唯一の方法は、リルーパーとスタッキファイアによって放出されたパターンを検出し、それらを既約CFGに変換することです。 AFAIKが還元可能な制御フローのみをサポートするV8を開発している場合を除き、還元不可能な制御フローをサポートすることは純粋にメリットです。これにより、フロントエンドとバックエンドの両方が非常に簡単になります(フロントエンドは、内部に格納されているのと同じ形式でコードを出力できますが、バックエンドはそうではありません。パターンを検出する必要はありません)制御フローが削減できない場合はより良い出力を生成し、制御フローが削減できる通常の場合と同じかそれ以上の出力を生成します。
さらに、GCCとGoがWebAssemblyの作成を開始できるようになります。
V8はWebAssemblyエコシステムの重要なコンポーネントであることは知っていますが、現在の制御フローの状況から恩恵を受けるのはそのエコシステムの唯一の部分のようです。他のすべてのバックエンドは、とにかくCFGに変換され、影響を受けません。 WebAssemblyが還元不可能な制御フローを表すことができるかどうか。
v8は、入力CFGを受け入れるためにrelooperを組み込むことができませんでしたか? v8の実装の詳細では、エコシステムの大部分がブロックされているようです。
参考までに、c ++のswitchステートメントはwasmで非常に遅いことに気づきました。 コードのプロファイルを作成するとき、画像処理を行うためにはるかに高速に動作する他の形式にコードを変換する必要がありました。 そして、それは他のアーキテクチャでは決して問題ではありませんでした。 パフォーマンス上の理由から、gotoが本当に欲しいです。
@graph 、「switchステートメントが遅い」方法について詳しく教えてください。 常にパフォーマンスを向上させる機会を探しています...(このスレッドを停止したくない場合は、直接私にメールしてください、lhansen @ mozilla.com)。
これはすべてのブラウザに適用されるため、ここに投稿します。 emscriptenでコンパイルした場合のこのような単純なステートメントは、ifステートメントに変換した方が高速です。
for(y = ....) {
for(x = ....) {
switch(type){
case IS_RGBA:....
....
case IS_BGRA
....
case IS_RGB
....
....
コンパイラがジャンプテーブルをwasmがサポートするものに変換していたと思います。 生成されたアセンブリを調べなかったため、確認できません。
Webでの画像処理用に最適化できるwasmとは関係のないカップルを知っています。 Firefoxの「フィードバック」ボタンから送信しました。 興味のある方はお知らせください。問題をメールでお知らせします。
@graph完全なベンチマークはここで非常に役立ちます。 一般に、Cのスイッチは、wasmで非常に高速なジャンプテーブルに変わる可能性がありますが、LLVMまたはブラウザーのいずれかで修正する必要がある、まだうまく機能しないコーナーケースがあります。
特にemscriptenでは、スイッチの処理方法が古いfastcompバックエンドと新しいアップストリームバックエンドの間で大きく異なるため、これを少し前に見た場合、または最近、fastcompを使用している場合は、アップストリームを確認することをお勧めします。
@ graph 、emscriptenがbr_tableを生成する場合、jitはジャンプテーブルを生成することがあり、(より高速であると思われる場合は)キースペースを線形またはインラインバイナリ検索で検索することもあります。 多くの場合、それが行うことはスイッチのサイズによって異なります。 もちろん、選択ポリシーが最適でない可能性もあります... @ kripkenに同意します。共有するものがある場合は、実行可能なコードがここで非常に役立ちます。
(v8またはjscについてはわかりませんが、Firefoxは現在if-then-elseチェーンを可能なスイッチとして認識しないため、通常、if-then-elseチェーンである限りスイッチをオープンコードすることはお勧めできません。損益分岐点は、おそらく2つか3つの比較にすぎません。)
@ lars -t-hansen @graphこの交換が示しているように見えるので、 br_table
は現在非常に最適化されていない可能性があります: https :
@aardappel 、それは不思議です、昨日実行したベンチマークはこれを示していませんでした。私のシステムのFirefoxでは、損益分岐点は私が覚えているように約5ケースで、その後br_tableが勝者でした。 もちろんマイクロベンチマークであり、ルックアップキーの均等な分散を試みています。 「if」ネストが最も可能性の高いキーに偏っていて、2、3回のテストしか必要ない場合は、「if」ネストが優先されます。
それを回避するためにスイッチ値の範囲分析を実行できない場合、br_tableは、スイッチの範囲に対して少なくとも1つのフィルタリングテストも実行する必要があります。これも利点になります。
@ lars-t-hansenはい、彼のテストケースはわかりません。外れ値があった可能性があります。 いずれにせよ、ChromeにはFirefoxよりもやるべきことがたくさんあるようです。
私は休暇中なので、返事がありません。 理解に感謝。
@kripken @
Main.cpp
#include <stdio.h>
#include <chrono>
#include <random>
class Chronometer {
public:
Chronometer() {
}
void start() {
mStart = std::chrono::steady_clock::now();
}
double seconds() {
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
}
private:
std::chrono::steady_clock::time_point mStart;
};
int main() {
printf("Starting tests!\n");
Chronometer timer;
// we want to prevent optimizations based on known size as most applications
// do not know the size in advance.
std::random_device rd; //Will be used to obtain a seed for the random number engine
std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
std::uniform_int_distribution<> dis(100000000, 1000000000);
std::uniform_int_distribution<> opKind(0, 3);
int maxFrames = dis(gen);
int switchSelect = 0;
constexpr int SW1 = 1;
constexpr int SW2 = 8;
constexpr int SW3 = 32;
constexpr int SW4 = 38;
switch(opKind(gen)) {
case 0:
switchSelect = SW1;
break;
case 1:
switchSelect = SW2; break;
case 2:
switchSelect = SW3; break;
case 4:
switchSelect = SW4; break;
}
printf("timing with SW = %d\n", switchSelect);
timer.start();
int accumulator = 0;
for(int i = 0; i < maxFrames; ++i) {
switch(switchSelect) {
case SW1:
accumulator = accumulator*3 + i; break;
case SW2:
accumulator = (accumulator < 3)*i; break;
case SW3:
accumulator = (accumulator&0xFF)*i + accumulator; break;
case SW4:
accumulator = (accumulator*accumulator) - accumulator + i; break;
}
}
printf("switch time = %lf seconds\n", timer.seconds());
printf("accumulated value: %d\n", accumulator);
timer.start();
accumulator = 0;
for(int i = 0; i < maxFrames; ++i) {
if(switchSelect == SW1)
accumulator = accumulator*3 + i;
else if(switchSelect == SW2)
accumulator = (accumulator < 3)*i;
else if(switchSelect == SW3)
accumulator = (accumulator&0xFF)*i + accumulator;
else if(switchSelect == SW4)
accumulator = (accumulator*accumulator) - accumulator + i;
}
printf("if-else time = %lf seconds\n", timer.seconds());
printf("accumulated value: %d\n", accumulator);
return 0;
}
switchSelectの値によって異なります。 if-elseはパフォーマンスを上回ります。 出力例:
Starting tests!
timing with SW = 32
switch time = 2.049000 seconds
accumulated value: 0
if-else time = 0.401000 seconds
accumulated value: 0
switchSelect = 32でわかるように、if-elseの方がはるかに高速です。 その他の場合、if-elseの方が少し高速です。 switchSelect = 1&0の場合、switchステートメントの方が高速です。
Test in Firefox 69.0.3 (64-bit)
compiled using: emcc -O3 -std=c++17 main.cpp -o main.html
emcc version: emcc (Emscripten gcc/clang-like replacement) 1.39.0 (commit e047fe4c1ecfae6ba471ca43f2f630b79516706b)
2019年10月20日現在の最新の安定したemscripenを使用しています。新規インストール./emcc activate latest
。
上記でタイプミスがあることに気づきましたが、同じ命令を実行しているので、if-elseの方がSW3の場合の方が速いという問題には影響しないはずです。
繰り返しますが、これは損益分岐点の5を超えています。興味深いことに、この場合のswitchSelect = 32の場合、速度はif-elseと同じです。 1003でわかるように、if-elseの方がわずかに高速です。 この場合、スイッチが勝つはずです。
Starting tests!
timing with SW = 1003
switch time = 2.253000 seconds
accumulated value: 1903939380
if-else time = 2.197000 seconds
accumulated value: 1903939380
main.cpp
#include <stdio.h>
#include <chrono>
#include <random>
class Chronometer {
public:
Chronometer() {
}
void start() {
mStart = std::chrono::steady_clock::now();
}
double seconds() {
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
}
private:
std::chrono::steady_clock::time_point mStart;
};
int main() {
printf("Starting tests!\n");
Chronometer timer;
// we want to prevent optimizations based on known size as most applications
// do not know the size in advance.
std::random_device rd; //Will be used to obtain a seed for the random number engine
std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
std::uniform_int_distribution<> dis(100000000, 1000000000);
std::uniform_int_distribution<> opKind(0, 8);
int maxFrames = dis(gen);
int switchSelect = 0;
constexpr int SW1 = 1;
constexpr int SW2 = 8;
constexpr int SW3 = 32;
constexpr int SW4 = 38;
constexpr int SW5 = 64;
constexpr int SW6 = 67;
constexpr int SW7 = 1003;
constexpr int SW8 = 256;
switch(opKind(gen)) {
case 0:
switchSelect = SW1;
break;
case 1:
switchSelect = SW2; break;
case 2:
switchSelect = SW3; break;
case 3:
switchSelect = SW4; break;
case 4:
switchSelect = SW5; break;
case 5:
switchSelect = SW6; break;
case 6:
switchSelect = SW7; break;
case 7:
switchSelect = SW8; break;
}
printf("timing with SW = %d\n", switchSelect);
timer.start();
int accumulator = 0;
for(int i = 0; i < maxFrames; ++i) {
switch(switchSelect) {
case SW1:
accumulator = accumulator*3 + i; break;
case SW2:
accumulator = (accumulator < 3)*i; break;
case SW3:
accumulator = (accumulator&0xFF)*i + accumulator; break;
case SW4:
accumulator = (accumulator*accumulator) - accumulator + i; break;
case SW5:
accumulator = (accumulator << 3) - accumulator + i; break;
case SW6:
accumulator = (i - accumulator) & 0xFF; break;
case SW7:
accumulator = i*i + accumulator; break;
}
}
printf("switch time = %lf seconds\n", timer.seconds());
printf("accumulated value: %d\n", accumulator);
timer.start();
accumulator = 0;
for(int i = 0; i < maxFrames; ++i) {
if(switchSelect == SW1)
accumulator = accumulator*3 + i;
else if(switchSelect == SW2)
accumulator = (accumulator < 3)*i;
else if(switchSelect == SW3)
accumulator = (accumulator&0xFF)*i + accumulator;
else if(switchSelect == SW4)
accumulator = (accumulator*accumulator) - accumulator + i;
else if(switchSelect == SW5)
accumulator = (accumulator << 3) - accumulator + i;
else if(switchSelect == SW6)
accumulator = (i - accumulator) & 0xFF;
else if(switchSelect == SW7)
accumulator = i*i + accumulator;
}
printf("if-else time = %lf seconds\n", timer.seconds());
printf("accumulated value: %d\n", accumulator);
return 0;
}
これらのテストケースをご覧いただきありがとうございます。
これは非常にまばらなswitch
ですが、LLVMはとにかくif-thenのセットと同等に変換する必要がありますが、手動のif-thenよりも効率が悪い方法で変換するようです。 wasm2watを実行して、これら2つのループのコードがどのように異なるかを確認しましたか?
これは、各反復で同じ値を使用するこのテストにも強く依存します。 このテストは、すべての値を循環する場合、またはさらに良いことに、それらからランダムに選択された場合に適しています(安価に実行できる場合)。
さらに良いことに、人々がパフォーマンスのためにスイッチを使用する本当の理由は、範囲が狭いためです。したがって、実際にその下でbr_table
を使用していることを保証できます。 br_table
がif
よりも速いケースがいくつあるかを確認することは、知っておくと最も便利なことです。
タイトループのスイッチが使用されたのは、パフォーマンスよりもコードがクリーンだったためです。 しかし、wasmの場合、パフォーマンスへの影響が大きすぎるため、より醜いifステートメントに変換されました。 多くのユースケースでの画像処理では、スイッチのパフォーマンスを向上させたい場合は、スイッチをループの外に移動し、各ケースのループのコピーを用意します。 通常、切り替えは、ピクセル形式、カラー形式、エンコーディングなどの形式を切り替えるだけです。多くの場合、定数は、線形ではなく、定義または列挙型を介して計算されます。 私の問題は後藤デザインとは関係がないことがわかりました。 switchステートメントで何が起こっているのかについての理解が不完全でした。 このような場合に画像処理のwasmを最適化するために、これを読んでいるブラウザ開発者にとって、私のメモが役立つことを願っています。 ありがとう。
gotoがこんなに白熱した議論になるとは思ってもみませんでした😮。 私はすべての言語のボートに乗っているので、gotoが必要です😁。 gotoを追加するもう1つの理由は、コンパイラーがwasmにコンパイルする複雑さを軽減することです。 私はそれがどこかで上で言及されているとかなり確信しています。 今、私は不満を言うことは何もありません😞。
そこにさらなる進歩はありますか?
激しい議論のために、一部のブラウザは非標準のバイトコード拡張としてgotoのサポートを追加すると思います。 そうすれば、GCCは非標準バージョンをサポートするものとしてゲームに参加できるかもしれません。 これは全体的には良いとは思いませんが、コンパイラの競争が増えるでしょう。 これは考慮されましたか?
最近はあまり進展がありませんが、ファンクレットの提案をご覧ください。
@graph to me、あなたの提案は「すべてを壊して、より良いものを期待しましょう」のように聞こえます。
それはそのようには機能しません。 現在のWebAssembly構造には多くの利点があります(残念ながら、それは明らかではありません)。 wasmの哲学を深く掘り下げてみてください。
「任意のラベルとGoto」を許可すると、検証不可能なバイトコードの(古代の)時代に戻ることができます。 すべてのコンパイラは、「正しく実行する」のではなく、「怠惰な方法」に切り替えるだけです。
現在の状態のwasmにいくつかの大きな欠落があることは明らかです。 人々は( @binjiによって言及されたような)ギャップを埋めるために取り組んでいますが、「グローバルなwasm構造」を作り直す必要
@vshymanskyy任意のラベルやgotoと同等の機能を提供するファンクレットの提案は、線形時間で完全に検証可能です。
また、線形時間のWasmコンパイラでは、すべてのWasm制御フローをファンクレットのような表現に内部的にコンパイルします。このブロックポストにいくつかの情報があり、Wasm制御フローからこの内部表現への変換が実装されています。ここ。 コンパイラは、この関数のような表現からすべての型情報を取得するため、線形時間で型の安全性を検証するのは簡単だと言えば十分です。
還元不可能な制御フローを線形時間で検証できないというこの誤解は、JVMに起因していると思います。この場合、還元不可能な制御フローは、コンパイルではなくインタープリターを使用して実行する必要があります。 これは、JVMには既約制御フローの型メタデータを表す方法がないため、スタックマシンからレジスタマシンへの変換を実行できないためです。 「任意のgoto」(つまり、バイト/命令Xにジャンプ)はまったく検証できませんが、関数を型付きブロックに分割し、任意の順序でジャンプできるようにすることは、モジュールを型付き関数に分割するよりも検証が難しくありません。 、その後、任意の順序でジャンプできます。 GCCやLLVMなどのコンパイラーによって発行される有用なパターンを実装するために、jump-to-byte-Xスタイルの型指定されていないgotoは必要ありません。
ここでのプロセスが大好きです。 サイドAは、これが特定のアプリケーションで必要な理由を説明しています。 サイドBは、彼らがそれを間違っていると言っていますが、そのアプリケーションのサポートを提供していません。 サイドAは、Bの実用的な議論のどれも水を保持しない方法を説明します。 サイドBは、サイドAが間違っていると考えているため、これに対処することを望んでいません。 サイドAは目標を達成しようとしています。 サイドBは、それが怠惰または残忍だと言って、それは間違った目標だと言います。 より深い哲学的意味はサイドAで失われます。語用論はサイドBで失われます。なぜなら、彼らはある種のより高い道徳的根拠を持っていると主張しているからです。 サイドAは、これを不道徳な機械的操作と見なしています。 最終的に、サイドBは通常、良くも悪くもスペックを制御し続け、相対的な純度で信じられないほどの量を達成しました。
正直なところ、私はここに鼻を突っ込んでいました。何年も前に、ESP8266をターゲットとするESP8266で開発環境を実行できるように、WASMへのTinyCCポートを作成しようとしていたからです。 私は最大4MBのストレージしか持っていないので、リルーパーとASTへの切り替え、およびその他の多くの変更を含めることは問題外です。 (補足:relooperはrelooperのような唯一のものですか?それはすごいひどいです、そして誰もその吸盤をIn Cで書き直しませんでした!?)この時点で可能であったとしても、私がTinyCCターゲットを書くかどうかはわかりませんWASMは、私にとってもうそれほど面白くないので。
ただし、このスレッド。 この糸は私に実存的な喜びをもたらしました。 人類の分岐点を監視することは、民主党や共和党、あるいは宗教よりも深く実行されます。 これが解決できたらと思います。 AがBの世界に住むようになるか、Bが手続き型プログラミングにその場所があるというAの主張を検証することができれば...私たちは世界平和を解決できると感じています。
V8の担当者は、このスレッドで、還元不可能な制御フローに対する反対がV8の現在の実装の影響を受けていないことを確認できますか?
これが私を最も悩ませているので、私は尋ねています。 私には、これはこの機能の長所と短所についての仕様レベルでの議論であるように思われます。 特定の実装が現在どのように設計されているかにまったく影響されるべきではありません。 しかし、V8の実装がこれに影響を与えていると私に信じさせる声明がありました。 多分私は間違っています。 オープンステートメントが役立つ場合があります。
残念ながら、これまでに存在していた現在の実装は非常に重要であるため、将来(おそらく過去よりも長い)はそれほど重要ではありません。 私は#1202で、一貫性がいくつかの実装よりも重要であることを説明しようとしていましたが、私は妄想しているようです。 あるプロジェクトのどこかでいくつかの開発決定が普遍的な真実を構成していない、またはデフォルトで正しいと仮定されなければならないことを説明する幸運。
このスレッドは、W3C炭鉱のカナリアの1つです。 私は多くのW3Cの個人に大きな敬意を払っていますが、JavaScriptをW3CではなくEcma Internationalに委託するという決定は、偏見なく行われたものではありません。
@cnlohrのように、私はTCC wasmポートに期待を持っていましたが、それには正当な理由があります。
「Wasmはプログラミング言語のポータブルコンパイルターゲットとして設計されており、クライアントおよびサーバーアプリケーションのWebでの展開を可能にします。」 --webassembly.org
確かに、 goto
が[専門用語を挿入]である理由は誰でも理解できますが、意見よりも標準を優先するのはどうでしょうか。 POSIX Cが優れたベースラインターゲットであることに同意することができます。特に、今日のlangはCから作成されているか、Cに対してベンチマークされており、WASMのホームページの見出しはそれ自体をlangのポータブルコンパイルターゲットと宣伝しています。 確かに、いくつかの機能はスレッドやsimdのようにロードマップされます。 しかし、 goto
ような基本的なものを完全に無視し、ロードマッピングの品位を与えないことは、WASMの表明された目的と、 <marquee>
をグリーンライト化する標準化団体からのそのようなスタンスと一致しません。淡い色を超えています。
SEI CERT C Coding StandardRecによるとMEM12-Cのタイトル、 「リソースの使用および解放時に関数をエラーのままにする場合は、gotoチェーンの使用を検討してください」 ;
多くの機能では、複数のリソースを割り当てる必要があります。 割り当てられたすべてのリソースを解放せずに失敗してこの関数の途中で戻ると、メモリリークが発生する可能性があります。 この方法でリソースの1つ(またはすべて)を解放するのを忘れることはよくあるエラーです。したがって、gotoチェーンは、解放されたリソースの順序を維持しながら出口を編成するための最も簡単でクリーンな方法です。
次に、推奨事項は、 goto
を使用した推奨POSIXCソリューションの例を示しています。 否定論者は、 goto
は依然として有害であると考えられていることに注意するでしょう。 興味深いことに、この意見は、これらの特定のコーディング標準の1つに具体化されておらず、単なるメモです。 それは私たちをカナリア、「有害であると考えられている」に連れて行きます。
結論として、「CSSリージョン」またはgoto
を有害であると見なす場合は、そのような機能が使用される問題に対する提案された解決策と一緒に検討する必要があります。 上記の「有害な」機能を削除することが、代替手段なしで合理的なユースケースを削除することになる場合、それは解決策ではなく、実際には言語のユーザーに有害です。
Cでも、関数はゼロコストではありません。誰かがgotosとlabelsの代わりを提供する場合は、canihazをお願いします。 誰かが私がそれを必要としないと言った場合、彼らはどうやってそれを知っていますか? パフォーマンスに関しては、 goto
を使用すると、言語の黎明期から存在していた、パフォーマンスが高く、簡単に作成できる機能は必要ないという、エンジニアに少し余分で議論するのは難しいでしょう。
goto
をサポートする計画がなければ、WASMはおもちゃのコンパイルターゲットであり、それは問題ありません。おそらく、W3CがWebを見る方法です。 標準としてのWASMが、32ビットアドレス空間からさらに高くなり、コンパイル競争に参加することを願っています。 WASMは素晴らしいはずなので、エンジニアリングの言説が「それは不可能です...」から抜け出して、 Labels asValuesのようなGCCC拡張機能を早急に追跡できることを願っています。 個人的には、TCCはこの時点でかなり印象的で便利であり、流行に敏感なランディングページや光沢のあるロゴがなくても、無駄な宣伝は一切ありません。
@ d4tocchini :
SEI CERT C Coding StandardRecによるとMEM12-Cのタイトル、 「リソースの使用および解放時に関数をエラーのままにする場合は、gotoチェーンの使用を検討してください」 ;
多くの機能では、複数のリソースを割り当てる必要があります。 割り当てられたすべてのリソースを解放せずに失敗してこの関数の途中で戻ると、メモリリークが発生する可能性があります。 この方法でリソースの1つ(またはすべて)を解放するのを忘れることはよくあるエラーです。したがって、gotoチェーンは、解放されたリソースの順序を維持しながら出口を編成するための最も簡単でクリーンな方法です。
次に、推奨事項は、
goto
を使用した推奨POSIXCソリューションの例を示しています。 否定論者は、goto
は依然として有害であると見なされていることに注意するでしょう。 興味深いことに、この意見は、これらの特定のコーディング標準の1つに具体化されているのではなく、単なるメモです。 それは私たちをカナリア、「有害であると考えられている」に連れて行きます。
その推奨事項に示されている例は、Wasmで利用可能なラベル付きの区切りで直接表現できます。 任意後藤の余計な力は必要ありません。 (Cはラベル付きのブレークアンドコンティニューを提供しないため、必要以上に頻繁にgotoにフォールバックする必要があります。)
@rossberg 、その例のラベル付きブレークの良い点は提案された機能を拒否し、 Sun JVMとデフォルトのCPythonの両方がCで記述されていることを考えると、サポートされる言語としてCが優先リストの上位にあるべきであることに同意しませんか?
goto
すぐに検討対象から外す場合は、emscriptenのソース内でのgoto
の
Cで書けない言語はありますか? 言語としてのCは、WASMの機能を通知する必要があります。 今日のWASMでPOSIXCが不可能な場合は、適切なロードマップがあります。
議論の話題ではありませんが、一般的な議論のあちこちにランダムな間違いが潜んでいることを隠さないでください。
Pythonはブレークにラベルを付けました
詳細を教えていただけますか? (別名:Pythonにはラベル付きのブレークがありません。)
@pfalcon 、はい、私の悪いです、私はPythonが提案したラベル付きのブレーク/継続を明確にするためにコメントを編集し、
gotoをすぐに検討対象から外す場合は、emscriptenのソース内でのgotoの何百もの使用も再検討する必要がありますか?
1)emscriptenに直接存在するのではなく、musllibcにどれだけ存在するかに注意してください。 (2番目に使用されるのはtests / third_partyです)
2)ソースレベルの構成はバイトコード命令と同じではありません
3)Emscriptenは、wasm標準と同じレベルの抽象化ではないため、それに基づいて再検討する必要はありません。
具体的には、今日、libcからgotoを書き直すと便利な場合があります。そうすれば、relooper / cfgstackifyを信頼して適切に処理するよりも、結果のcfgをより細かく制御できるからです。 上流のmuslからの非常に異なるコードで終わるのは、取るに足らない量の作業であるため、私たちはそうしていません。
Emscripten開発者(私が最後にチェックした)は、これらの明白な理由から、gotoのような構造は本当に素晴らしいと思う傾向があるため、許容できる妥協点に到達するのに何年もかかるとしても、それを検討から外す可能性は低いです。
<marquee>
を青信号にするという標準化団体からのそのようなスタンスは、淡い色を超えています。
これは特に不吉な声明です。
1)We-the-broader-Internetは、その決定を下すまでに10年以上かかります
2)We-the-wasm-CGは、そのタグとは完全に(ほぼ?)別のグループであり、おそらく過去の明らかな間違いにも個別に悩まされています。
流行に敏感なランディングページと光沢のあるロゴなしで、無駄な宣伝なしで。
これは、トーンの問題に遭遇することなく、「私はイライラしている」と言い換えることができたはずです。
このスレッドが示すように、これらの会話はそれなりに難しいものです。
深く信頼され理解されている一連の関数を、それらを使用するための環境がそれをサポートするために追加の手順を経なければならないという理由だけで、すべての新しいものに書き直したい場合、新しいレベルの深い懸念があります。 (私はまだしっかりとお願いします-追加-gotoキャンプにいますが、特定のコンパイラを1つだけ使用することに縛られるのは嫌いです)
このスレッドは生産性をはるかに超えたと思います。4年以上実行されており、任意のgoto
に対する賛否両論がここで使用されているようです。 これらの議論はどれも特に新しいものではないことにも注意する必要があります;)
任意のジャンプラベルを持たないことを選択したマネージドランタイムがありますが、これは問題なく機能しました。 また、任意のジャンプが許可されているプログラミングシステムもあり、それらもうまく機能しています。 結局、プログラミングシステムの作成者は設計の選択を行い、それらの選択が成功したかどうかを実際に示すのは時間だけです。
任意のジャンプを禁止するWasmデザインの選択は、その哲学の中核です。 純粋な間接ジャンプをサポートしていないのと同じ理由で、ファンクレットのようなものがなくてもgoto
サポートできる可能性は低いです。
任意のジャンプを禁止するWasmデザインの選択は、その哲学の中核です。 純粋な間接ジャンプをサポートしていないのと同じ理由で、ファンクレットのようなものがなくてもgotoをサポートできる可能性は低いです。
@penznなぜfuncletsの提案が行き詰まっているのですか? 2018年10月から存在し、まだフェーズ0です。
ありふれたオープンソースプロジェクトについて話し合っていたら、私はそれをフォークしてやり遂げるでしょう。 ここでは、広範囲にわたる独占基準について話し合っています。 私たちが気にかけているので、活発なコミュニティの反応を育む必要があります。
@ J0eCool
- その多くがemscriptenに直接存在するのではなく、musllibcに存在することに注意してください。 (2番目に使用されるのはtests / third_partyです)
はい、一般的にCでどれだけ使用されているかについてはうなずきました。
- ソースレベルの構成はバイトコード命令と同じではありません
もちろん、私たちが議論しているのは、ソースレベルの構成に影響を与える内部の懸念です。 それは欲求不満の一部です、ブラックボックスはその懸念を漏らすべきではありません。
- Emscriptenは、wasm標準と同じレベルの抽象化ではないため、それに基づいて再検討する必要はありません。
重要なのは、WebAssemblyツールチェーン全体の中でも、大部分のサイズの大きいCプロジェクトでgoto
が見つかるということです。 独自のコンパイラをターゲットにするのに十分な表現力を持たない一般的な言語のポータブルコンパイラターゲットは、企業の性質と正確に一致していません。
具体的には、今日、libcからgotoを書き直すと便利な場合があります。そうすれば、relooper / cfgstackifyを信頼して適切に処理するよりも、結果のcfgをより細かく制御できるからです。
これは円形です。 上記の多くは、そのような要件の不可謬性に関して深刻な未回答の質問を提起しました。
上流のmuslからの非常に異なるコードで終わるのは、取るに足らない量の作業であるため、私たちはそうしていません。
あなたが言ったように、gotosを削除することは可能です、それは取るに足らない量の仕事です! gotosはサポートされるべきではないので、他のすべての人がコードパスを乱暴に分岐させるべきだと提案していますか?
Emscripten開発者(私が最後にチェックした)は、これらの明白な理由から、gotoのような構造は本当に素晴らしいと思う傾向があるため、許容できる妥協点に到達するのに何年もかかるとしても、それを検討から外す可能性は低いです。
希望のきらめき! ロードマップアイテムと、たとえ何年も経っていても、ボールを動かすための公式の招待状で、goto / labelのサポートが真剣に受け止められれば満足です。
これは特に不吉な声明です。
あなたが正しい。 誇張を許してください、私は少しイライラしています。 私はwasmが大好きで、頻繁に使用しますが、ポートTCCのように、wasmで注目に値することをしたい場合は、最終的に目の前に痛みの道が見えます。 すべてのコメントと記事を読んだ後でも、反対派が技術的であるか、哲学的であるか、政治的であるかを理解することはできません。 @neelanceが表現したように、
「V8の担当者は、このスレッドで、還元不可能な制御フローに対する反対がV8の現在の実装の影響を受けていないことを確認できますか?
これが私を最も悩ませているので、私は尋ねています。 [...]
いずれかの使用法を聞いている場合は、Go1.11に関する@neelanceのフィードバックを心に留めてください。 それについて議論するのは難しいです。 確かに、私たちは皆、gotoの重要なダスティングを行うことができますが、それでも、goto命令でのみ修正できる深刻なパフォーマンスヒットを受け取ります。
繰り返しになりますが、私のフラストレーションをお許しください。ただし、この問題が適切なアドレスなしでクローズされた場合、この種のコミュニティの対応を悪化させるだけの間違った種類のシグナルを送信し、私たちの最大の標準化の取り組みの1つには不適切になるのではないかと心配しています。分野。 言うまでもなく、私はこのチームの大ファンであり、サポーターです。 ありがとう!
goto / funcletsの欠落によって引き起こされる別の現実の問題は次のとおりです: https :
このプログラムの場合、Goコンパイラは現在、18,000個のネストされたblock
持つwasmバイナリを生成します。 wasmバイナリ自体のサイズは2.7MBですが、 wasm2wat
を実行すると、4.7GBの.watファイルが得られます。 🤯
Goコンパイラにヒューリスティックを与えることを試みることができるので、単一の巨大なジャンプテーブルの代わりに、ある種のバイナリツリーを作成し、ジャンプターゲット変数を複数回調べることができます。 しかし、これは本当にwasmであるはずの方法ですか?
ただ1つのコンパイラ(Emscripten [1])だけがWebAssemblyを現実的にサポートできれば、人々がそれが完全に問題ないと考えるのは奇妙だと思うことを付け加えたいと思います。
自由奔放な状況(著作権で保護されたコードに規範的に依存する標準)をいくらか思い出させます。
また、WebAssemblyの開発者がこれに非常に熱心に反対しているように見えるのも奇妙だと思いますが、コンパイラー側のほぼ全員が必要だと言っています。 注意:WebAssemblyは標準であり、マニフェストではありません。 実際、最近のほとんどのコンパイラーは、明示的なループの概念を持たない、何らかの形式のSSA +基本ブロックを内部的に(または同じプロパティを持つほぼ同等のもの)使用します[2]。 JITでさえ似たようなものを使用しますが、それは一般的です。
「gotoを使用するだけ」のエスケープハッチなしでリループが発生するための絶対的な要件は、私の知る限り[3]、言語から言語への翻訳者以外では前例のないことです。 gotoのない言語をターゲットにします。 特に、WebAssembly以外のあらゆる種類のIRまたはバイトコードに対してこれを実行する必要があるとは聞いたことがありません。
おそらく、WebAssemblyの名前をWebEmscripten(WebScripten?)に変更するときです。
@ d4tocchiniが言ったように、WebAssemblyの(標準化の状況のために必要な)独占的ステータスがなければ、コンパイラ開発者がすでにサポートする必要があることを合理的にサポートできるものに、今では分岐している可能性があります。
いいえ、「emscriptenを使用するだけ」は、標準が単一のコンパイラベンダーに依存するため、有効な反論ではありません。 なぜそれが悪いのかを言う必要がないことを願っています。
編集:私は1つのことを追加するのを忘れました:
問題が技術的、哲学的、または政治的であるかどうかについては、まだ明確にされていません。 私は後者を疑っていますが、喜んで間違っていることが証明されます(技術的および哲学的な問題は政治的問題よりもはるかに簡単に修正できるため)。
goto / funcletsの欠落によって引き起こされる別の現実の問題は次のとおりです:
このプログラムの場合、Goコンパイラは現在、18,000個のネストされた
block
持つwasmバイナリを生成します。 wasmバイナリ自体のサイズは2.7MBですが、wasm2wat
を実行すると、4.7GBの.watファイルが得られます。 🤯Goコンパイラにヒューリスティックを与えることを試みることができるので、単一の巨大なジャンプテーブルの代わりに、ある種のバイナリツリーを作成し、ジャンプターゲット変数を複数回調べることができます。 しかし、これは本当にwasmであるはずの方法ですか?
この例は本当に興味深いものです。 このような単純な直線プログラムはどのようにしてこのコードを生成しますか? 配列要素の数とブロックの数の関係は何ですか? 特に、これを、各配列要素へのアクセスには_複数の_ブロックを忠実にコンパイルする必要があることを意味すると解釈する必要がありますか?
いいえ、「emscriptenを使用するだけ」は有効な反論ではありません
この点での本当の反論は、Wasmをターゲットにしたい別のコンパイラが独自のリルーパーのようなアルゴリズムを実装できる/実装しなければならないということだと思います。 個人的には、Wasmは最終的にマルチボディループ(ファンクレットに近い)またはgoto
自然なターゲットである同様のものを持つべきだと思います。
@ conrad-watt各割り当てがCFGでいくつかの基本ブロックを使用する原因となるいくつかの要因があります。 そのうちの1つは、コンパイル時に長さがわからないため、スライスに長さチェックがあることです。 一般的に、コンパイラーは基本ブロックを比較的安価な構成と見なしますが、特にこの特定のケースでは、wasmではいくらか高価です。
コードが複数の関数に分割されている変更された例の@neelanceでは、(ランタイム/コンパイル)メモリのオーバーヘッドがはるかに低いことが示されています。 この場合、生成されるブロックは少なくなりますか、それとも、個別の関数がエンジンGCをより細かくできることを意味するだけですか?
@ conrad-wattメモリを使用しているのはGoコードではなく、WebAssemblyホストです。Chrome86でwasmバイナリをインスタンス化すると、CPUが2分間100%になり、タブのメモリ使用量がピークに達します。 11.3GB。 wasmバイナリ/ゴーコードが実行される前にこれがあります。 問題を引き起こしているのは、wasmバイナリの形状です。
それはすでに私の理解でした。 多数のブロック/型アノテーションが、特にコンパイル/インスタンス化中にメモリオーバーヘッドを引き起こすと予想します。
私の前の質問を明確にするために-コードの分割バージョンがより少ないブロックでWasmにコンパイルされる場合(リルーパーの癖のため)、それはメモリオーバーヘッドの削減の1つの説明であり、より一般的なものを追加するための良い動機になりますWasmへの制御フロー。
あるいは、分割されたコードの結果、ブロックの総数が(ほぼ)同じになる可能性がありますが、各関数は個別にJITコンパイルされるため、各関数のコンパイルに使用されるメタデータ/ IRはWasmエンジンによってより熱心にGCされる可能性があります。 同様の問題は、V8年前に大きなasm.js関数を解析/コンパイルしたときに発生しました。 この場合、より一般的な制御フローをWasmに導入しても、問題は解決しません。
最初に明確にしておきたいのは、Goコンパイラはrelooperアルゴリズムを使用していないことです。これは、ゴルーチンの切り替えの概念と本質的に互換性がないためです。 すべての基本ブロックは、可能な場合は少しフォールスルーするジャンプテーブルを介して表現されます。
ネストされたblock
の深さに関して、Chromeのwasmランタイムは指数関数的に複雑になっていると思います。 分割バージョンのブロック数は同じですが、最大深度が小さくなっています。
この場合、より一般的な制御フローをWasmに導入しても、問題は解決しません。
この複雑さの問題は、おそらくChromeの側で解決できることに同意します。 しかし、私はいつも「なぜこの問題がそもそも存在したのか」という質問をするのが好きです。 より一般的な制御フローでは、この問題は存在しなかったと思います。 また、すべての基本ブロックがジャンプテーブルとして表現されているため、一般的なパフォーマンスのオーバーヘッドがかなりあります。これは、最適化によって解消される可能性は低いと思います。
ネストされたブロックの深さに関して、Chromeのwasmランタイムは指数関数的に複雑になっていると思います。 分割バージョンのブロック数は同じですが、最大深度が小さくなっています。
これは、N個の配列アクセスを持つ直線関数で、最終的な配列アクセスがNブロックの深さでネストされる(一定の係数)ことを意味しますか? もしそうなら、エラー処理コードを別の方法で因数分解することによってこれを減らす方法はありますか? 3000個のネストされたループを分析する必要がある場合(非常に大まかなアナロジー)、コンパイラーがチャグすることを期待します。したがって、これがセマンティック上の理由で避けられない場合、それはより一般的な制御フローの議論にもなります。
ネストの違いがそれよりもそれほど大きくない場合、私の勘では、V8は単一のWasm関数のコンパイル中のメタデータのGCをほとんど行わないため、最初から言語で調整されたファンクレットの提案のようなものがあったとしても、興味深いGC最適化を行わなくても、同じオーバーヘッドが表示されます。
また、すべての基本ブロックがジャンプテーブルとして表現されているため、一般的なパフォーマンスのオーバーヘッドがかなりあります。これは、最適化によって解消される可能性は低いと思います。
ここでより自然なターゲットを持つことが(純粋に技術的な観点から)明らかに好ましいことに同意します。
これは、N個の配列アクセスを持つ直線関数で、最終的な配列アクセスがNブロックの深さでネストされる(一定の係数)ことを意味しますか? もしそうなら、エラー処理コードを別の方法で因数分解することによってこれを減らす方法はありますか? 3000個のネストされたループを分析する必要がある場合(非常に大まかなアナロジー)、コンパイラーがチャグすることを期待します。したがって、これがセマンティック上の理由で避けられない場合、それはより一般的な制御フローの議論にもなります。
逆:最初の割り当ては、最後ではなく、深くネストされています。 ネストされたblock
と上部の単一のbr_table
は、従来のswitch
ステートメントがwasmで表現される方法です。 これは私が言及したジャンプテーブルです。 3000のネストされたループはありません。
ネストの違いがそれよりもそれほど大きくない場合、私の勘では、V8は単一のWasm関数のコンパイル中にメタデータのGCをほとんど行わないため、最初から言語で調整されたファンクレットの提案のようなものがあったとしても、興味深いGC最適化を行わなくても、同じオーバーヘッドが表示されます。
はい、基本ブロックの数に関して指数関数的に複雑な実装もあるかもしれません。 しかし、基本ブロックを(大量であっても)処理することは、多くのコンパイラーが1日中行うことです。 たとえば、Goコンパイラ自体は、いくつかの最適化パスによって処理される場合でも、コンパイル中にこの数の基本ブロックを簡単に処理します。
はい、基本ブロックの数に関して指数関数的に複雑な実装もあるかもしれません。 しかし、基本ブロックを(大量であっても)処理することは、多くのコンパイラーが1日中行うことです。 たとえば、Goコンパイラ自体は、いくつかの最適化パスによって処理される場合でも、コンパイル中にこの数の基本ブロックを簡単に処理します。
もちろんですが、ここでのパフォーマンスの問題は、これらの基本ブロック間の制御フローが元のソース言語でどのように表現されるかと直交します(つまり、Wasmでのより一般的な制御フローの動機ではありません)。 ここでV8が特に悪いかどうかを確認するには、FireFox / SpiderMonkeyまたはLucet / Craneliftが同じコンパイルオーバーヘッドを示すかどうかを確認できます。
私はさらにいくつかのテストを行いました。FirefoxとSafariはまったく問題を示していません。 興味深いことに、Chromeは集中的なプロセスが終了する前にコードを実行することもできるため、wasmバイナリの実行に厳密に必要ではないタスクに複雑さの問題があるようです。
もちろんですが、ここでのパフォーマンスの問題は、これらの基本ブロック間の制御フローが元のソース言語でどのように表現されるかと直交します。
あなたの言ってる事がわかります。
基本ブロックをジャンプ命令ではなく、ジャンプ変数と巨大なジャンプテーブル/ネストされたブロックで表現することは、基本ブロックの単純な概念を非常に複雑な方法で表現していると私は今でも信じています。 これにより、パフォーマンスのオーバーヘッドが発生し、ここで見たような複雑さの問題が発生するリスクがあります。 複雑なシステムよりも単純なシステムの方が優れており、堅牢であると私は信じています。 より単純なシステムが悪い選択であると私に納得させる議論を見たことがありません。 V8は任意の制御フローを実装するのに苦労すると聞いただけで、このステートメントが間違っていることを伝えるための私の未解決の質問(https://github.com/WebAssembly/design/issues/796#issuecomment-623431527)はそうではありませんでしたまだ答えられました。
@neelance
Chromeは、集中的なプロセスが完了する前にコードを実行することもできます
ベースラインコンパイラのLiftoffは問題ないようですが、問題は最適化コンパイラTurboFanにあります。 問題を提出するか、テストケースを提供してください。必要に応じて提出できます。
より一般的には、 wasmスタックの切り替え計画でGoの日常的な実装の問題を解決できると思いますか? これは私が見つけることができる最良のリンクですが、隔週の会議と、作業を動機付けるいくつかの強力なユースケースがあり、現在非常に活発です。 Goが大きなスイッチパターンを回避するためにwasmコルーチンを使用できる場合、任意のgotoは必要ないと思います。
Goコンパイラは、ゴルーチンの切り替えの概念と本質的に互換性がないため、relooperアルゴリズムを使用していません。
確かにそれだけでは適用できません。 ただし、wasm構造化制御フロー+ Asyncifyを使用すると良好な結果が得られます。 大きなスイッチを1つも使用せずに、通常のwasm制御フロー(if、ループなど)を可能な限り放出し、そのパターンの上にインストルメンテーションを追加して、スタックの巻き戻しと巻き戻しを処理するという考え方があります。 これにより、コードサイズがかなり小さくなり、非スタックスイッチングコードは基本的にフルスピードで実行できますが、実際のスタックスイッチは多少遅くなる可能性があります(したがって、これは、各ループの反復でスタックスイッチが常に発生しない場合などに適しています。 。)。
興味があれば、Goでそれを試してみてください。 これは明らかに、wasmに組み込まれているスタックスイッチングのサポートほど良くはありませんが、すでに大きなスイッチパターンよりも優れている可能性があります。 また、後で組み込みのスタック切り替えサポートに切り替える方が簡単です。 具体的には、この実験がどのように機能するかは、スタックの切り替えをまったく気にせずにGoに通常の構造化コードを発行させ、適切なポイントで特別なmaybe_switch_goroutine
関数の呼び出しを発行することです。 Asyncifyトランスフォームは、基本的に残りすべてを処理します。
qemuなどの動的再コンパイルエミュレーター用のgotoに興味があります。 他のコンパイラーとは異なり、qemuはプログラム制御フロー構造の知識を持っていないため、gotosが唯一の妥当なターゲットです。 末尾呼び出しは、各ブロックを関数としてコンパイルし、各gotoを末尾呼び出しとしてコンパイルすることで、これに対処できます。
@kripken非常に役立つ投稿をありがとうございます。
ベースラインコンパイラのLiftoffは問題ないようですが、問題は最適化コンパイラTurboFanにあります。 問題を提出するか、テストケースを提供してください。必要に応じて提出できます。
ここだwasmバイナリあなたが実行することができますwasm_exec.htmlが。
wasmスタックの切り替え計画により、Goの日常的な実装の問題を解決できると思いますか?
はい、一見するとこれが役立つようです。
ただし、wasm構造化制御フロー+非同期を使用すると良好な結果が得られます。
これも有望に見えます。 Goでリルーパーを実装する必要がありますが、それで問題ないと思います。 小さな欠点の1つは、wasmバイナリを生成するためにbinaryenに依存関係が追加されることです。 私はおそらくすぐに提案を書くでしょう。
https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2を実装したい場合は、LLVMのスタッキファイアアルゴリズムの方が簡単/優れていると思い
Goプロジェクトの提案を提出しました: https :
@ neelance 、 @ kripkenの提案がgolang + wasmに少し役立つのを見て
リンクリストに対するLinusTorvaldsの「GoodTaste」の議論が、唯一の特殊なケースの分岐ステートメントを削除するという優雅さに基づいている場合、この種の特殊な体操を勝利または正しい方向への一歩と見なすことは困難です。 Cの非同期のようなAPIにgotosを個人的に使用して、goto命令があらゆる種類の匂いをトリガーする前に、スタックの切り替えについて話しました。
私が誤解している場合は訂正してください。しかし、提起されたいくつかの質問に対するわずかな特殊性に焦点を当てた一見フライバイの応答を除いて、ここのメンテナは目前の問題について明確に説明しておらず、難しい質問にも答えていないようです。 敬意を表して、この緩慢な骨化はカルス企業政治の特徴ではありませんか? もしそうなら、私は窮状を理解しています... ANSI Cだけが互換性のあるリトマス試験であった場合、Wasmのブランドがサポートを誇ることができるすべての言語/コンパイラを想像してみてください!
@neelance @darkuranium @ d4tocchiniすべてのWasm寄稿者が、gotoの欠如が正しいことだと思っているわけではありません。実際、私はそれをWasmの#1の設計ミスと個人的に評価します。私はそれを(ファンクレットとしてまたは直接)追加することに絶対に賛成です。
ただし、このスレッドで議論しても、gotosが発生することはなく、Wasmに関係するすべての人があなたのために仕事をすることを魔法のように確信させることもありません。実行する手順は次のとおりです。
@ d4tocchini正直なところ、私は現在、提案された解決策を「変更できない状況を考えると、前進するための最良の方法」、別名「回避策」と@kripkenに感謝します。)
@aardappel私の知る限り、 @ sunfishcodeはfuncletsの提案をプッシュしようとしましたが、失敗しました。 なぜ私にとっては違うのでしょうか?
@neelance @sunfishcodeは、最初の作成を超えて提案をプッシュする時間があまりなかったと思います。そのため、提案は「失敗」ではなく「停止」します。 私が指摘しようとしていたように、パイプラインを完全に通過するための提案には、チャンピオンが継続的な作業を行う必要があります。
@neelance
テストケースをありがとう! 同じ問題をローカルで確認できます。 https://bugs.chromium.org/p/v8/issues/detail?id=11237を提出しました
Go [..]にrelooperを実装する必要があります。小さな欠点の1つは、wasmバイナリを生成するためにbinaryenに依存関係が追加されることです。
ところで、それが助けになるなら、binaryenのライブラリビルドを単一のCファイルとして作成することができます。 多分それは統合するのが簡単ですか?
また、Binaryenを使用すると、そこにあるRelooper実装を使用できます。 IRの基本ブロックを渡して、再ループを実行させることができます。
@taralx
LLVMのスタッキファイアアルゴリズムはより簡単で優れていると思いますが、
リンクはアップストリームLLVMに関するものではなく、Cheerpコンパイラ(LLVMのフォーク)であることに注意してください。 それらのStackifierの名前はLLVMと似ていますが、異なります。
Cheerpの投稿は、2011年の元のアルゴリズムを参照していることにも注意してください。最新のリルーパーの実装(前述のとおり)には、長年にわたって言及されている問題はありません。 Cheerpや他の人たちがやっていることと非常によく似ている、その一般的なアプローチのより単純でより良い代替案を私は知りません-これらはテーマのバリエーションです。
@kripken問題を提出していただきありがとうございます。
ところで、それが助けになるなら、binaryenのライブラリビルドを単一のCファイルとして作成することができます。 多分それは統合するのが簡単ですか?
ありそうもない。 Goコンパイラ自体は少し前に純粋なGoに変換されており、他のC依存関係を使用していません。 これも例外ではないと思います。
ファンクレットの提案の現状は次のとおりです。プロセスの次のステップは、ステージ1に入るCG投票を要求することです。
私自身、現在WebAssemblyの他の領域に焦点を当てており、ファンクレットを前進させるための帯域幅がありません。 ファンクレットのチャンピオンの役割を引き継ぐことに興味がある人がいれば、喜んで引き継ぎます。
ありそうもない。 Goコンパイラ自体は少し前に純粋なGoに変換されており、他のC依存関係を使用していません。 これも例外ではないと思います。
さらに、これは、WebAssemblyランタイムで深刻なパフォーマンスの低下を引き起こすリルーパーの広範な使用の問題を解決しません。
@Vurich
これは、wasmにgotoを追加するための最良のケースかもしれないと思いますが、誰かがそのような深刻なパフォーマンスの崖を示す実際のコードから説得力のあるデータを収集する必要があります。 私自身はそのようなデータを見たことがありません。 「それほど速くない:WebAssemblyとネイティブコードのパフォーマンスの分析」(2019)のようなwasmパフォーマンスの欠陥を分析する作業は、制御フローが重要な要素であることもサポートしていません(分岐命令の量が多いことに気づいていますが、そうではありません構造化された制御フローによるものです-むしろ安全チェックによるものです)。
@kripkenそのようなデータを収集する方法について何か提案はありますか? パフォーマンスの不足が構造化された制御フローによるものであることをどのように示すでしょうか。
ここでの不満の一部であるコンパイル段階のパフォーマンスを分析する多くの作業がある可能性は低いです。
まだswitchcase構造がないことに少し驚いていますが、ファンクレットはそれを包含しています。
@neelance
ええ、特定の原因を理解するのは簡単ではありません。 たとえば、境界チェックの場合、VMでそれらを無効にして測定することができますが、残念ながら、gotosに対して同じことを行う簡単な方法はありません。
1つのオプションは、放出されたマシンコードを手作業で比較することです。これは、リンクされた紙で行ったことです。
もう1つのオプションは、制御フローを最適に処理できると思われるものにwasmをコンパイルすることです。つまり、構造化を「元に戻す」ことです。 LLVMはそれを実行できるはずなので、LLVM(WAVMやwasmerなど)を使用するVMで、またはWasmBoxCを介してwasmを実行することは興味深いかもしれません。 LLVMでCFG最適化を無効にして、それがどれほど重要かを確認することもできます。
@taralx
興味深いことに、コンパイル時やメモリ使用量について何か見落としていましたか? 構造化された制御フローは、実際にはより優れているはずです。たとえば、一般的なCFGからと比較して、そこからSSAフォームに移動するのは非常に簡単です。 これは、実際、wasmが最初に構造化された制御フローを使用した理由の1つでした。 これは、Webでの読み込み時間に影響するため、非常に注意深く測定されます。
(または、開発者のマシンでのコンパイラーのパフォーマンスを意味しますか?wasmは、クライアントではなく、そこでより多くの作業を行う方向に傾いているのは事実です。)
エンベッダーでのコンパイルパフォーマンスを意味しましたが、それはバグとして扱われているようで、必ずしも純粋なパフォーマンスの問題ではありませんか?
@taralx
はい、それはバグだと思います。 これは、1つのVMの1つの層で発生します。 そして、その根本的な理由はありません。構造化された制御フローは、より多くのリソースを必要とせず、より少ないリソースを必要とするはずです。 つまり、wasmにgotoがあれば、そのようなパフォーマンスのバグが発生する可能性が高くなると思います。
@kripken
構造化された制御フローは、実際にはより優れているはずです。たとえば、一般的なCFGからと比較して、そこからSSAフォームに移動するのは非常に簡単です。 これは、実際、wasmが最初に構造化された制御フローを使用した理由の1つでした。 これは、Webでの読み込み時間に影響するため、非常に注意深く測定されます。
念のため、非常に具体的な質問です。実際にそれを実行するWasmコンパイラを知っていますか。「構造化制御フロー」からSSA形式への「非常に単純な」ものです。 簡単に見ると、Wasmの制御フローは(完全に/最終的に)構造化されていないためです。 正式に構造化されたコントロールは、 break
s、 continue
s、 return
sがないコントロールです(大まかに言って、Schemeのプログラミングモデルであり、call / ccのような魔法はありません)。 それらが存在する場合、そのような制御フローは大まかに「半構造化」と呼ぶことができます。
完全に構造化された制御フローのためのよく知られたSSAアルゴリズムがあります:
構造化ステートメントについては、解析中に1回のパスでSSAフォームとドミネーターツリーの両方を生成する方法を示しました。 次のセクションでは、任意のポイントで制御構造からの終了を引き起こす可能性のある特定のクラスの非構造化ステートメント(LOOP / EXITおよびRETURN)にメソッドを拡張することさえ可能であることを示します。 ただし、そのような出口は一種の(統制のとれた)gotoであるため、構造化されたステートメントよりも処理がはるかに難しいことは驚くべきことではありません。
OTOH、もう1つのよく知られたアルゴリズム、 https: //pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdfがあり
したがって、問題は、あるプロジェクトがBrandis /Mössenböckアルゴリズムを実際に拡張する問題を通り抜け、Braun etalと比較してそのルートで具体的な利益を達成したことを知っているかどうかです。 アルゴリズム(補足として、私の直感的な予感は、ブラウンアルゴがまさにそのような「上界」の拡張であるということですが、正式な証明について話すのではなく、自分自身に直感的に証明するにはあまりにも愚かなので、それはそれです-直感的な予感)。
そして、質問の一般的なテーマは、Wasmが任意のgotoサポートをオプトアウトした最終的な理由を確立することです(私は「維持する」と言いますが)。 このスレッドを何年も見ていたので、私が構築したメンタルモデルは、既約CFGに直面することを避けるために行われたというものです。 実際、この溝は還元可能CFGと不可能CFGの間にあり、多くの最適化アルゴリズムは還元可能CFGにとって(はるかに)簡単です。これが多くのオプティマイザーがコーディングしたものです。Wasmの(半)構造化制御フローは、保証するための安価な方法です。既約。
構造化CFG(およびWasm CFGは実際には正式な意味で構造化されていないようです)のSSA作成の特別な容易さについて言及すると、上記の明確な状況がどういうわけか曇っています。 そのため、SSAの構築がWasmCFGフォームによって実際に恩恵を受けているという特定の参照があるかどうかを尋ねています。
ありがとう。
@kripken私は今少し混乱していて、学びたいと思っています。 私は状況を見ています、そして私は現在以下を見ます:
プログラムのソースには、特定の制御フローがあります。 このCFGは、還元可能であるか、そうでないかのいずれかです。たとえば、gotoがソース言語で使用されているかどうかは関係ありません。 この事実を変える方法はありません。 このCFGは、たとえばGoコンパイラがネイティブに行うように、マシンコードに変換できます。
CFGがすでに削減可能である場合、すべてが順調であり、wasmVMはCFGをすばやくロードできます。 すべての翻訳パスは、これが単純なケースであることを検出し、迅速な処理を実行できる必要があります。 還元不可能なCFGを許可しても、このケースの速度が低下することはありません。
CFGが削減できない場合は、次の2つのオプションがあります。
コンパイラは、ジャンプテーブルを導入するなどして、それを削減可能にします。 この手順では情報が失われます。 バイナリを生成したコンパイラに固有の分析がなければ、元のCFGを復元することは困難です。 この情報の損失のため、生成されるマシンコードは、最初のCFGから生成されるコードよりもいくらか遅くなります。 このマシンコードはシングルパスアルゴリズムで生成できる可能性がありますが、情報が失われるという犠牲が伴います。 [1]
コンパイラが既約CFGを出力できるようにします。 VMはそれを削減可能にする必要があるかもしれません。 これにより、ロード時間が遅くなりますが、CFGが実際に削減できない場合に限ります。 コンパイラーには、ロード時のパフォーマンスを最適化するか、実行時のパフォーマンスを最適化するかを選択するオプションがあります。
[1]操作を元に戻す方法がまだある場合、それは実際には情報の損失ではないことを認識していますが、それをより適切に説明することはできませんでした。
私の思考の欠陥はどこにありますか?
@pfalcon
実際にそれを行うWasmコンパイラを知っていますか?「構造化制御フロー」からSSA形式への「非常に単純な」ものです。
VMについて:直接はわかりません。 しかし、当時のIIRCは、 @ titzerと@lukewagnerが、そのように実装するのが便利だと言っていました。おそらく、そのうちの1つで詳しく説明できます。 還元不可能性が全体の問題であったかどうかはわかりません。 そして、彼らがあなたが言及したそれらのアルゴリズムを実装したかどうかはわかりません。
VM以外のことについて:Binaryenオプティマイザーは、削減可能であるというだけでなく、wasmの構造化された制御フローから確実に恩恵を受けています。 たとえば、wasmで注釈が付けられているループヘッダーがどこにあるかを常に把握しているため、さまざまな最適化が簡単になります。 (OTOHの他の最適化は実行が難しく、それらのための一般的なCFG IRもあります...)
@neelance
CFGがすでに削減可能である場合、すべてが順調であり、wasmVMはCFGをすばやくロードできます。 すべての翻訳パスは、これが単純なケースであることを検出し、迅速な処理を実行できる必要があります。 還元不可能なCFGを許可しても、このケースの速度が低下することはありません。
多分私はあなたを完全に理解していません。 ただし、wasm VMがコードをすばやくロードできるかどうかは、コードが削減可能かどうかだけでなく、コードのエンコード方法にも依存します。 具体的には、一般的なCFGであるフォーマットを想像することができ、VMはそれが削減可能であることを確認するための作業を行う必要があります。 Wasmはその作業を回避することを選択しました-エンコーディングは必然的に削減可能です(つまり、Wasmを読んで簡単な検証を行うと、余分な作業を行わなくても削減可能であることが証明されます)。
さらに、wasmのエンコーディングは、それを検証する必要なしに、削減可能性を保証するだけではありません。 また、ループヘッダー、if、およびその他の便利なものに注釈を付けます(このコメントの前半で個別に言及したように)。 実稼働VMがどれだけのメリットを享受できるかはわかりませんが、メリットがあると思います。 (おそらく特にベースラインコンパイラでは?)
全体として、既約CFGを許可すると、既約CFGが別の方法でエンコードされない限り(ファンクレットが提案されているように)、高速のケースが遅くなる可能性があると思います。
@kripken
説明ありがとうございます。
はい、これはまさに私が作ろうとしている差別化です。還元可能なCFGの場合の構造化表記/エンコーディングの利点がわかります。 しかし、既約CFGの表記を可能にし、
結論として、純粋に還元可能な表記法の方が速いと主張する方法がわかりません。 還元可能なソースCFGの場合、それは同じくらい高速です。 そして、既約ソースCFGの場合、それはそれほど遅くはないと主張することができますが、実際のいくつかのケースでは、これが一般的にはありそうもないことをすでに示しています。
つまり、パフォーマンスの考慮事項が、還元不可能な制御フローを妨げる議論になる可能性があることはわかりません。そのため、次のステップでパフォーマンスデータを収集する必要があるのはなぜか疑問に思います。
@neelance
はい、ファンクレットのような新しい構成を追加できることに同意します。それを使用しないことで、既存のケースの速度が低下することはありません。
ただし、新しい構成を追加すると、wasmが複雑になるため、欠点があります。 特に、VMの表面積が大きくなることを意味し、バグやセキュリティの問題が発生する可能性が高くなります。 Wasmは、VMの複雑さを軽減するために、開発者側で可能な限り複雑にすることに傾倒しています。
一部のwasm提案は、GC(JSでのサイクル収集を可能にする)のように、速度だけではありません。 しかし、ファンクレットのように速度に関する提案の場合、速度が複雑さを正当化することを示す必要があります。 SIMDについてもこの議論がありましたが、実際のコード(2倍以上)で非常に大きなスピードアップを確実に達成できることがわかったので、それだけの価値があると判断しました。
(一般的なCFGを許可する速度以外にも、コンパイラがwasmをターゲットにしやすくするなどの利点があります。ただし、wasm VMを複雑にすることなく、これを解決できます。LLVMとBinaryenで任意のCFGのサポートをすでに提供しています。 、コンパイラーがCFGを発行し、構造化された制御フローについて心配する必要がないようにします。それでも不十分な場合は、私たち(私を含むツールの人々)はもっと多くのことを行う必要があります。)
Funcletは、重要な制御フローを持つ言語をWebAssemblyにコンパイルできるようにすることほど速度ではありません。最も明白なのは、CとGoですが、async / awaitを持つすべての言語に適用されます。 また、V8以外のすべてのWasmコンパイラが階層制御フローをCFGに分解するという事実からも明らかなように、階層制御フローを選択すると、実際にはVMに_more_バグが発生します。 CFGのEBBは、Wasmなどの複数の制御フロー構造を表すことができます。コンパイルする単一の構造を持つことで、さまざまな用途のさまざまな種類を持つよりもはるかに少ないバグが発生します。
非常に単純なストリーミングコンパイラであるLightbeamでさえ、制御フローをCFGに分解する追加の変換ステップを追加した後、誤コンパイルのバグが大幅に減少しました。 これは、このプロセスの反対側では2倍になります-Relooperは、ファンクレットを発行するよりもはるかにエラーが発生しやすく、LLVMのWasmバックエンドに取り組んでいる開発者や、実装されるファンクレットである他のコンパイラーは、 codegenの信頼性と単純さを向上させるために、funcletのみを使用して機能します。 Wasmを生成するすべてのコンパイラはEBBを使用し、Wasmを消費するコンパイラの1つを除くすべてがEBBを使用します。ファンクレットまたはCFGを表す他の方法を実装することを拒否すると、V8チーム以外の関係者全員に害を及ぼす損失の多いステップが間に追加されます。 。
「有害と見なされる還元不可能な制御フロー」は単なる論点です。ファンクレットの制御フローを還元可能にするという制限を簡単に追加できます。将来、還元不可能な制御フローを許可したい場合は、還元可能な制御フローを備えた既存のすべてのWasmモジュールが変更されずに機能します。還元不可能な制御フローを追加でサポートするエンジン。 バリデーターの還元性チェックを削除するだけの場合です。
@Vurich
ファンクレットの制御フローを削減できるという制限を簡単に追加できます
可能ですが、それは簡単ではありません。VMはそれを確認する必要があります。 これは、現在ほとんどのVMに存在するベースラインコンパイラーにとって問題となる単一の線形パスでは不可能だと思います。 (実際、ループのバックエッジを見つけるだけでは(これはより単純な問題であり、他の理由でも必要です)、1回のフォワードパスでは実行できませんか?)
V8以外のすべてのWasmコンパイラは、とにかく階層制御フローをCFGに分解します。
TurboFanが使用する「ノードの海」アプローチについて言及していますか? 私はその専門家ではないので、他の人に返答を任せます。
しかし、より一般的には、コンパイラーを最適化するために上記の議論を購入しなくても、前述のように、ベースライン・コンパイラーにはさらに直接的に当てはまります。
Funcletは、重要な制御フローを持つ言語をWebAssemblyにコンパイルできるようにすることほど速度ではありません[..] Relooperは、Funcletを発行するよりもはるかにエラーが発生しやすいです。
ツール側で100%同意します。 ほとんどのコンパイラから構造化コードを発行するのは難しいです! しかし、重要なのは、VM側でそれがより簡単になるということであり、それがwasmが選択したことです。 しかし、繰り返しになりますが、これには、あなたが言及した欠点を含め、トレードオフがあることに同意します。
2015年にwasmはこれを間違えましたか? それが可能だ。 自分自身でいくつかの問題が発生したと思います(デバッグ可能性やスタックマシンへの切り替えが遅れたなど)。 しかし、振り返ってそれらを修正することは不可能であり、新しいもの、特に重複するものを追加するための高い基準があります。
それを踏まえて、建設的になるように努めるなら、ツール側の既存の問題を修正する必要があると思います。 ツールの変更には、はるかに低い基準があります。 2つの可能な提案:
ファンクレットなどは、純粋にツール側で実装できます。 つまり、今日はこのためのライブラリコードを提供していますが、バイナリ形式を追加することもできます。 (wasmオブジェクトファイルで、ツール側のwasmバイナリ形式に追加する前例がすでにあります。)
これについて具体的な作業が行われている場合、(AFAIU)これをWasmに追加するための最小の慣用的な方法( @rossbergがほのめかしているように)は、ブロック命令を導入することです。
マルチループ(t in ) _n_ t out (_instr_ * end ) _n_
これは、n個のラベル付きボディを定義します(n個の入力型アノテーションが前方宣言されています)。 次に、 brファミリの命令が一般化され、マルチループによって定義されたすべてのラベルが各ボディ内で順番に範囲内になります(マルチループ本体がに分岐すると、実行は本体の_start_にジャンプします(通常のWasmループのように)。 実行が別のボディに分岐せずにボディの終わりに達すると、構成全体が返されます(フォールスルーなし)。
各ボディの型アノテーションを効率的に表現する方法については、いくつかのバイクシェディングが必要になります(上記の定式化では、n個のボディにn個の異なる入力タイプを含めることができますが、すべて同じ出力タイプである必要があるため、直接使用することはできません余分な感じのLUB計算を必要としない通常の複数値の_blocktype_インデックス)、および実行する最初の本体を選択する方法(常に最初、または静的パラメーターがあるべきですか?)。
これにより、ファンクレットと同じレベルの表現度が得られますが、制御命令の新しいスペースを導入する必要がなくなります。 実際、ファンクレットをさらに繰り返していたら、こんな感じになっていたと思います。
編集:フォールスルー動作を持つようにこれを調整すると、正式なセマンティクスがわずかに複雑になりますが、 @ neelanceのユースケースにはおそらくより適切であり、ベースラインコンパイラにオントレース制御フローパスが何であるかを示唆するのに役立ちます。
エンジンをよりシンプル/高速にするためにツールに作業をオフロードするというWasmの設計原則は非常に重要であり、今後も非常に有益です。
とは言うものの、重要なすべてのものと同様に、それは白黒ではなくトレードオフです。 ここでは、生産者の苦痛がエンジンの苦痛に不釣り合いな場合があると思います。 Wasmに導入したいほとんどのコンパイラは、内部で任意のCFG構造(SSA)を使用するか、goto(CPU)を気にしないものをターゲットにするために使用されます。 私たちは、それほど多くの利益を得るために、世界をフープを飛び越えさせています。
ファンクレット(またはマルチループ)のようなものはモジュール式であるため便利です。プロデューサーがそれを必要としない場合は、以前と同じように機能します。 エンジンが実際に任意のCFGを処理できない場合は、今のところ、それがloop
+ br_table
種類の構成であるかのように出力でき、それを使用するものだけが代償を払います。 。 次に、「市場が決定」し、エンジンに対してより良いコードを発行するよう圧力がかかっているかどうかを確認します。 ファンクレットに依存するWasmコードがたくさんあるとしたら、エンジンが適切なコードを出力することは、一部の人が考えているほど大きな災害にはならないだろうと、何かが教えてくれます。
可能ですが、それは簡単ではありません。VMはそれを確認する必要があります。 これは、現在ほとんどのVMに存在するベースラインコンパイラーにとって問題となる単一の線形パスでは不可能だと思います。
ベースラインコンパイラへの期待を誤解しているかもしれませんが、なぜ彼らは気にするのでしょうか? gotoが表示された場合は、ジャンプ命令を挿入します。
ツール側で100%同意します。 ほとんどのコンパイラから構造化コードを発行するのは難しいです! しかし、重要なのは、VM側でそれがより簡単になるということであり、それがwasmが選択したことです。 しかし、繰り返しになりますが、これには、あなたが言及した欠点を含め、トレードオフがあることに同意します。
いいえ、元のコメントで何度も言っているように、VM側での作業が簡単になるわけではありません。 私は1年以上ベースラインコンパイラに取り組んでいましたが、Wasmの制御フローをCFGに変換する中間ステップを追加した後、私の生活は楽になり、出力されるコードは速くなりました。
可能ですが、それは簡単ではありません。VMはそれを確認する必要があります。 これは、現在ほとんどのVMに存在するベースラインコンパイラーにとって問題となる単一の線形パスでは不可能だと思います。 (実際、ループのバックエッジを見つけるだけでは(これはより単純な問題であり、他の理由でも必要です)、1回のフォワードパスでは実行できませんか?)
さて、コンパイラで使用されるアルゴリズムに関する私の知識は、ストリーミングコンパイラで還元不可能な制御フローを検出できる、または検出できないことを絶対的に確実に述べるほど強力ではありませんが、そうである必要はありません。 検証は、コンパイルと並行して行うことができます。 ストリーミングアルゴリズムが存在しない場合(あなたも私もそれが存在しないことを私は知りません)、関数が完全に受信されたら、非ストリーミングアルゴリズムを使用できます。 (何らかの理由で)還元不可能な制御フローが無限ループのような本当に悪いものにつながる場合は、コンパイルをタイムアウトするか、コンパイルスレッドをキャンセルすることができます。 しかし、これが事実であると信じる理由はありません。
ベースラインコンパイラへの期待を誤解しているかもしれませんが、なぜ彼らは気にするのでしょうか? gotoが表示された場合は、ジャンプ命令を挿入します。
Wasmの無限レジスタマシン(いいえ、スタックマシンではありません)を物理ハードウェアの有限レジスタにマッピングする必要があるため、それほど単純ではあり
私が取り組んだストリーミングコンパイラは、任意の-既約でさえ-CFGを問題なくコンパイルできます。 特に特別なことは何もしていません。 最初にジャンプする必要があるとき、および条件付きで2つに分岐する必要があるポイントに到達した場合は、各ブロックに「呼び出し規約」(基本的にはそのブロックのスコープ内の値が存在する場所)を割り当てるだけです。互換性のない「呼び出し規約」を持つ複数のターゲットは、「アダプタ」ブロックをキューにプッシュし、次の可能なポイントでそれを発行します。 これは、還元可能および還元不可能な制御フローの両方で発生する可能性があり、どちらの場合もほとんど必要ありません。 前に述べたように、「既約の制御フローは有害であると考えられています」という議論は論点であり、技術的な議論ではありません。 制御フローをCFGとして表すと、ストリーミングコンパイラの記述がはるかに簡単になります。何度も言ったように、これは幅広い個人的な経験からわかっています。
既約の制御フローによって実装の記述が困難になる場合(私には考えられないことですが)は、スタブアウトしてエラーを返すことができます。別の非ストリーミングアルゴリズムが必要な場合は、その制御を100%確実に検出します。フローは既約であるため(したがって、誤って既約の制御フローを受け入れないようにするため)、ベースラインコンパイラ自体とは別に実行できます。 私は、その主題に関する権威であると信じる理由がある誰かから(彼らがこのスレッドに引きずり込まれたくないことを知っているので、彼らを呼び出すことは避けますが)、比較的単純なストリーミングアルゴリズムが存在すると言われましたCFGの還元不可能性を検出するためですが、これが真実であると直接言うことはできません。
@oridb
ベースラインコンパイラへの期待を誤解しているかもしれませんが、なぜ彼らは気にするのでしょうか? gotoが表示された場合は、ジャンプ命令を挿入します。
ベースラインコンパイラは、ループのバックエッジに追加のチェックを挿入するなどの処理を行う必要があるため(Webでは、ハングしたページで最終的に遅いスクリプトダイアログが表示されます)、そのようなことを識別する必要があります。 また、それらは適度に効率的なレジスタ割り当てを行おうとします(ベースラインコンパイラは、最適化コンパイラの約1/2の速度で実行されることがよくあります。これは、シングルパスであることを考えると非常に印象的です!)。 結合や分割を含む制御フローの構造を持つことで、それがはるかに簡単になります。
@gwvo
とは言うものの、重要なすべてのものと同様に、それは白黒ではなくトレードオフです。 [..]私たちは、それほど多くの利益を得るために、世界をフープを飛び越えさせています。
それはトレードオフであることに完全に同意し、おそらく当時は間違っていたのかもしれません。 しかし、ツール側でこれらのフープを修正する方がはるかに実用的だと思います。
次に、「市場が決定」し、エンジンに対してより良いコードを発行するよう圧力がかかっているかどうかを確認します。
これは実際、これまで回避してきたことです。 VM上でwasmを可能な限り単純にするように努めたため、複雑な最適化は必要ありません。インライン化なども可能な限り必要ありません。 目標は、VMに改善を迫ることではなく、ツール側でハードワークを実行することです。
@Vurich
私は1年以上ベースラインコンパイラに取り組んでいましたが、Wasmの制御フローをCFGに変換する中間ステップを追加した後、私の生活は楽になり、出力されるコードは速くなりました。
とても興味深い! それはどのVMでしたか?
また、シングルパス/ストリーミングであるかどうか(そうである場合、ループバックエッジインストルメンテーションをどのように処理したか)、およびレジスタ割り当てをどのように行うかについても特に興味があります。
原則として、ループバックエッジとレジスタ割り当ての両方は、基本ブロックが厳密に要求されることなく、合理的なトップソートのような順序で配置されることを期待して、線形命令順序に基づいて処理できます。
ループバックエッジの場合:バックエッジを、命令ストリームの前の方にジャンプする命令として定義します。 最悪の場合、ブロックが逆方向に配置されていると、厳密に必要とされるよりも多くのバックエッジチェックが行われます。
レジスタ割り当ての場合:これは単なる標準の線形スキャンレジスタ割り当てです。 レジスタ割り当ての変数の有効期間は、変数の最初の言及から最後の言及まで、その間に線形にあるすべてのブロックを含みます。 最悪の場合、ブロックがシャッフルされると、必要以上に寿命が長くなり、スタックに不必要に物をこぼしてしまいます。 唯一の追加コストは、各変数の最初と最後の言及を追跡することです。これは、1回の線形スキャンですべての変数に対して実行できます。 (wasmの場合、「変数」はローカルスロットまたはスタックスロットのいずれかであると思います。)
@comex
良い点!
唯一の追加コストは、各変数の最初と最後の言及を追跡することです
はい、それは大きな違いだと思います。 線形スキャンレジスタ割り当ては、 wasmベースラインコンパイラが現在実行しているものよりも優れています(ただし、実行は遅くなります)。これは、ストリーミング方式でコンパイルされるため、非常に高速です。 つまり、各変数の最後の言及を見つけるための最初のステップはありません-それらはシングルパスでコンパイルされ、構造によって助けられて、後でwasm関数でコードを見ることさえなく、コードを出力します。また、それらは単純になります彼らが行くにつれて選択(「愚かな」はその投稿で使用されている単語です)。
レジスタ割り当てに対するV8のストリーミングアプローチは、ブロックが相互再帰的であることが許可されている場合(https://github.com/WebAssembly/design/issues/796#issuecomment-742690194のように)、それらが処理する唯一のライフタイムであるため、同様に機能するはずです。単一のブロック内にバインドされている(スタック)か、関数全体(ローカル)であると見なされます。
IIUC( @titzerのコメントを参照)V8の主な問題は、ターボファンが最適化できるCFGの種類にあります。
@kripken
複雑な最適化を必要としないように、VMでwasmを可能な限りシンプルにするように努めました。
これは「複雑な最適化」ではありません。gotoは非常に基本的で、多くのシステムにとって自然です。 これを無料で追加できるエンジンはたくさんあると思います。 私が言っているのは、何らかの理由で構造化されたCFGモデルを保持したいエンジンがあれば、それは可能だということです。
たとえば、LLVM(現在のWasmプロデューサーの第1位)は、主要なエンジンでのパフォーマンスの低下がないと確信するまで、ファンクレットの使用に切り替えないだろうと確信しています。
@kripkenそれはWasmtimeの一部です。 はい、ストリーミングであり、O(N)の複雑さを意図していましたが、それが完全に実現する前に新しい会社に引っ越したので、それは「O(N)っぽい」だけです。 https://github.com/bytecodealliance/wasmtime/tree/main/crates/lightbeam
ありがとう@Vurich 、興味深い。 特にスタートアップだけでなくスループットについても、それらが利用可能であるときにパフォーマンス番号を確認するのは素晴らしいことです。 あなたのアプローチは、V8やSpiderMonkeyのエンジニアが採用したアプローチよりもコンパイルが遅く、より高速なコードを出力すると思います。 したがって、この分野では別のトレードオフになります。 あなたが言ったように、あなたのアプローチがwasmの構造化された制御フローの恩恵を受けていないのはもっともらしく思えますが、彼らのアプローチは恩恵を受けています。
いいえ、これはストリーミングコンパイラであり、これら2つのエンジンのいずれよりも高速にコードを出力します(ただし、プロジェクトを終了したときに修正されなかった縮退したケースがあります)。 私は高速コードを出力するために最善を尽くしましたが、それは主に、出力の効率が二次的な関心事であるコードを迅速に出力するように設計されています。 私の知る限り、すべてのデータ構造は初期化されずに開始され、コンパイルは命令ごとに行われるため、起動コストはゼロです(バックエンド間で共有されるWasmtimeの固有のコストを上回ります)。 手元にあるV8やSpiderMonkeyと比較するための数値はありませんが、Cranelift(wasmtimeのプライマリエンジン)と比較するための数値はあります。 現時点では数か月前のものですが、Craneliftよりも高速にコードを出力するだけでなく、Craneliftよりも高速にコードを出力することがわかります。 当時、それはSpiderMonkeyよりも高速なコードを放出していましたが、私の言葉を信じる必要があるので、あなたが私を信じていなくても私はあなたを責めません。 最近の数値はありませんが、現在の状態では、CraneliftとSpiderMonkeyの両方が、Lightbeamと比較した場合に、これらのマイクロベンチマークでの低パフォーマンス出力の主な原因である少数のバグを修正したと思います。しかし、各コンパイラは基本的に同じように設計されており、パフォーマンスのレベルが異なるのはそれぞれのアーキテクチャであるため、コンパイル速度の差はプロジェクトに参加している間ずっと変わりませんでした。 私はあなたの推測に感謝しますが、私が概説した方法がより遅くなるというあなたの仮定がどこから来ているのかわかりません。
ベンチマークは次のとおりです::compile
ベンチマークはコンパイル速度用であり、 ::run
ベンチマークはマシンコード出力の実行速度用です。 https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e
方法論はここにあります。クローンを作成してベンチマークを再実行し、結果を自分で確認できますが、PRは最新バージョンのwasmtimeと互換性がない可能性があるため、最後に更新したときのパフォーマンスの比較のみが表示されます。 PR。 https://github.com/bytecodealliance/wasmtime/pull/1660
そうは言っても、私の主張は、CFGがストリーミングコンパイラのパフォーマンスにとって有用な内部表現であるということではありません。 私の主張は、CFGはどのコンパイラのパフォーマンスにも悪影響を及ぼさず、GCCチームとGoチームをWebAssemblyの作成から完全にロックすることを正当化するレベルには決して影響しないということです。 ファンクレットまたはwasmの同様の拡張に反対するこのスレッドのほとんどの人は、この提案によって悪影響を受けると彼らが主張するプロジェクトに実際に取り組んだことはありません。 このトピックについてコメントするには、直接の経験が必要なことは言うまでもありませんが、誰もがある程度の貴重な意見を持っていると思いますが、自転車小屋の色について異なる意見を持つことと、作ることの間には線があります。ただの憶測に基づいた主張。
@Vurich
いいえ、これはストリーミングコンパイラであり、これら2つのエンジンのいずれよりも高速にコードを出力します(ただし、プロジェクトを離れたために修正されなかった縮退したケースがあります)。
以前に十分に明確でなかった場合は申し訳ありません。 同じことを話していることを確認するために、私はそれらのエンジンのベースラインコンパイラを意味しました。 そして、V8とSpiderMonkeyがこの用語を使用するという意味で、ベースラインコンパイラのポイントであるコンパイル時について話しています。
私がV8とSpiderMonkeyのベースラインコンパイル時間を打ち負かすことができるかどうか疑わしい理由は、前に示したリンクのように、これら2つのベースラインコンパイラがコンパイル時間に対して非常に調整されているためです。 特に、内部IRを生成せず、wasmからマシンコードに直接移行します。 コンパイラは内部IR(CFGの場合)を出力するとおっしゃいましたが、それだけでコンパイル時間が遅くなると思います(分岐、メモリ帯域幅などが多いため)。
ただし、これらのベースラインコンパイラに対してベンチマークを行ってください。 私の推測が間違っていることを示すデータを見たいのですが、V8とSpiderMonkeyのエンジニアもそうだと確信しています。 それはあなたが彼らが採用を検討すべきより良いデザインを見つけたことを意味するでしょう。
V8に対してテストするには、 d8 --liftoff --no-wasm-tier-up
を実行でき、SpiderMonkeyの場合はsm --wasm-compiler=baseline
実行できます。
(Craneliftと比較するための手順をありがとうございますが、Craneliftはベースラインコンパイラではないため、コンパイル時間を比較することはこのコンテキストでは関係ありません。それ以外の点では非常に興味深いですが、私は同意します。)
私の直感では、ベースラインコンパイラは、意味のあるブロック間最適化を実行しようとしないため、ファンクレット/マルチループをサポートするためにコンパイル戦略を大幅に変更する必要はありません。 @kripkenによって参照される信頼できる「結合と分割を含む制御フローの構造」は、相互再帰ブロックのコレクションのすべての入力タイプを前方宣言することを要求することで満たされます(とにかくストリーミング検証の自然な選択のようです) 。 Lightbeam / Wasmtimeがエンジンベースラインコンパイラを打ち負かすことができるかどうかは、これを考慮していません。 重要な点は、エンジンベースラインコンパイラが現在と同じ速度を維持できるかどうかです。
FWIW、私はこの機能が将来のCGミーティングで議論されるのを見たいと思っています。また、 Proper Tail Calls sagaのWebAssemblyバージョンを避けるように努めるべきであると直接会って意見を述べました)。 私は(現在非常に遅い)論文の提出を終えたら、新年に自分でそのようなCGディスカッションの最初の一歩を踏み出すことができてうれしいです。
@kripken
はい、それは大きな違いだと思います。 線形スキャンレジスタ割り当ては、 wasmベースラインコンパイラが現在実行しているものよりも優れています(ただし、実行は遅くなります)。これは、ストリーミング方式でコンパイルされるため、非常に高速です。 つまり、各変数の最後の言及を見つけるための最初のステップはありません-それらはシングルパスでコンパイルされ、構造によって助けられて、後でwasm関数でコードを見ることさえなく、コードを出力します。また、それらは単純になります彼らが行くにつれて選択(「愚かな」はその投稿で使用されている単語です)。
うわー、それは本当にとても簡単です。
一方、その特定のアルゴリズムは非常に単純である
ブログ投稿に記載されているように、SpiderMonkeyのwasmベースラインコンパイラは、固定ABIを使用するか、wasmスタックからネイティブスタックおよびレジスタにマッピングする代わりに、「制御フロー結合」(つまり、複数の先行ブロックを持つ基本ブロック)を介してレジスタアロケータの状態を保持しません。 ほとんどの場合、制御フロー結合ではありませんが、テストを通じて、ブロックに入るときに固定ABIも使用することがわかりまし
固定ABIは次のとおりです(x86の場合)。
rax
、およびx86にwasmスタック対応の残りスタック。なぜこれが重要なのですか?
このアルゴリズムは、はるかに少ない情報でほぼ同じように機能する可能性があるためです。 思考実験として、ネイティブアセンブリと同様に、構造化された制御フロー命令がなく、ジャンプ命令だけが存在するWebAssemblyの代替ユニバースバージョンを想像してみてください。 ジャンプの対象となる命令を特定する方法という1つの追加情報で補強する必要があります。
その場合、アルゴリズムは次のようになります。命令を直線的に実行します。 ジャンプおよびジャンプターゲットの前に、レジスタを固定ABIにフラッシュします。
1つの違いは、2つではなく1つの固定ABIが必要になることです。 スタックの最上位の値が意味的にジャンプの「結果」であるのか、外側のブロックからスタックに残されているだけなのかを区別できませんでした。 したがって、無条件にスタックの最上位をrax
ます。
しかし、これがパフォーマンスに測定可能なコストをもたらすとは思えません。 どちらかといえば、それは改善かもしれません。
(検証も異なりますが、それでもシングルパスです。)
さて、事前の警告:
これらを念頭に置いて、上記の思考実験は、ベースラインコンパイラが構造化された制御フローを必要としないという私の信念を強化します。 追加する構成の低レベルに関係なく、ジャンプターゲットである命令などの基本情報が含まれている限り、ベースラインコンパイラはわずかな変更でそれを処理できます。 または、少なくともこれは可能です。
@ conrad-watt @comex
それらは非常に良い点です! その場合、ベースラインコンパイラに関する私の直感は間違っている可能性があります。
そして@ comex-はい、あなたが言ったように、この議論は、SSAが構造から利益を得るかもしれないコンパイラの最適化とは別です。 以前のリンクの1つから少し引用する価値があるかもしれません:
設計上、WebAssemblyの構造化された制御フローのおかげで、WebAssemblyコードをTurboFanのIR(SSA構築を含む)に簡単なシングルパスで変換することは非常に効率的です。
@ conrad-watt私は、VMの人々から直接フィードバックを受け取り、心を開いておく必要があることに間違いなく同意します。 明確にするために、ここでの私の目標は何も止めることではありません。 いくつかのコメントは、wasmの構造化された制御フローは明らかな間違いであるか、ファンクレット/マルチループで明らかに修正する必要があると考えているようだったので、ここで詳しくコメントしました-ここで思考の歴史を提示したかっただけで、強い理由がありました現在のモデルの場合、改善するのは簡単ではないかもしれません。
この会話を読むのは本当に楽しかったです。 私はこれらの質問の多くを自分で(両方向から)疑問に思い、これらの考えの多くを(再び両方向から)共有しました。ディスカッションは多くの有用な洞察と経験を提供しました。 まだ強い意見があるかどうかはわかりませんが、それぞれの方向に貢献したいと思っています。
「for」側では、どのブロックにバックエッジがあるかを前もって知っておくと便利です。 ストリーミングコンパイラは、(ローカルで例えばインデックスWebAssemblyの型システムでは見かけないプロパティを追跡することができますi
ローカルで配列の境界内にあるarr
)。 前方にジャンプするときは、その時点で保持されているプロパティでターゲットに注釈を付けると便利な場合があります。 そうすれば、ラベルに到達したときに、すべてのインエッジにまたがるプロパティを使用してそのブロックをコンパイルできます。たとえば、配列境界チェックを排除できます。 しかし、ラベルが未知のバックエッジを持つ可能性がある場合、そのブロックはこの知識でコンパイルできません。 もちろん、非ストリーミングコンパイラは、より重要なループ不変分析を実行できますが、ストリーミングコンパイラの場合、何が先にあるかを心配する必要がないので便利です。 (副次的な考え: ローカルを使用しているため、スタックマシンではないと述べています。#1381で、ローカルへの依存を減らし、スタック操作を追加する理由をいくつか説明しました。レジスタ割り当てを簡単にすることも、その方向。)
「反対」の側では、これまでのところ、議論はローカルコントロールのみに焦点を合わせてきました。 これはCの場合は問題ありませんが、C ++や、同様の例外を除いた他のさまざまな言語の場合はどうでしょうか。 他の形式の非ローカル制御を備えた言語はどうですか? 動的スコープを持つものは、本質的に構造化されていることがよくあります(または、少なくとも相互再帰的な動的スコープの例はわかりません)。 これらの考慮事項は対処可能だと思いますが、結果をこれらの設定で使用できるようにするには、それらを念頭に置いて何かを設計する必要があります。 これは私が熟考してきたことであり、進行中の考え(@ conrad-wattのマルチループの拡張のように見えます)を興味のある人と共有できてうれしいです(ただし、ここではトピックから外れているようです)。少なくとも、覚えておくべきローカル制御フロー以上のものがあることを頭に入れておきたかったのです。
( @kripkenは考慮事項を表す素晴らしい仕事をしていると思いますが、VMの人々からより多くのことを聞くために、さらに+1を投入したいと思います。)
Lightbeamが内部IRを生成すると言うとき、それは本当に誤解を招くものであり、明確にする必要があります。 私はしばらくの間プロジェクトに取り組んでいました、そして時々あなたはトンネル視力を得ることができます。 基本的に、Lightbeamは入力命令を命令ごとに消費し(実際には最大で1つの命令の先読みがありますが、それは特に重要ではありません)、各命令に対して、遅延して一定のスペースで多数の内部IR命令を生成します。 Wasm命令ごとの命令の最大数は一定で、6のように少なくなっています。これは、関数全体のIR命令のバッファーを作成して、それに取り組んでいるわけではありません。 次に、それらのIR命令を1つずつ読み取ります。 本当に、各Wasm命令を実装する、より一般的なヘルパー関数のライブラリがあると考えることができます。これは、制御フローなどの異なるモデルがあることを説明するのに役立つため、IRと呼びます。おそらくV8やSpiderMonkeyのベースラインコンパイラほど速くコードを生成しませんが、それは完全に最適化されていないためであり、アーキテクチャ的に欠陥があるためではありません。 私のポイントは、LLVMやCraneliftのように実際にメモリ内にIRのバッファーを生成するのではなく、Wasmの階層制御フローをCFGであるかのように内部的にモデル化することです。
もう1つのオプションは、制御フローを最適に処理できると思われるものにwasmをコンパイルすることです。つまり、構造化を「元に戻す」ことです。 LLVMはそれを実行できるはずなので、LLVM(WAVMやwasmerなど)を使用するVMで、またはWasmBoxCを介してwasmを実行することは興味深いかもしれません。
@kripken残念ながら、LLVMはまだ構造化を元に戻すことができないようです。 ジャンプスレッディング最適化パスはこれを実行できるはずですが、このパターンはまだ認識されていません。 これは、リルーパーアルゴリズムがCFGをループ+スイッチに変換する方法を模倣するC ++コードを示す例です。 GCCはなんとかそれを「dereloop」しますが、clangはしません: https ://godbolt.org/z/GGM9rP
@AndrewScheidecker興味深い、ありがとう。 ええ、これはかなり予測できない可能性があるので、(以前にリンクされた「No So Fast」ペーパーのように)発行されたコードを調査し、LLVMのオプティマイザーに依存するようなショートカットの試みを避けるより良いオプションはないかもしれません。
@comex
SpiderMonkeyのベースラインコンパイラは1つの実装にすぎず、レジスタ割り当てに関して最適ではない可能性があります。少しスマートであれば、実行時のメリットがコンパイル時のコストを上回ります。
レジスタ割り当てについては明らかに賢明かもしれません。 それは、制御フローのフォーク、結合、および呼び出しの前に無差別にこぼれ、レジスターの状態に関するより多くの情報を維持し、レジスターの値をより長く/デッドになるまで保持しようとする可能性があります。 ブロックからの値の結果については、raxよりも優れたレジスタを選択するか、固定レジスタを使用しない方が適切です。 ローカル変数を保持するために、静的にいくつかのレジスターを割り当てることができます。 私が行ったコーパス分析では、ほとんどの関数には、ほんの数個の整数レジスタとFPレジスタで十分であることが示唆されました。 一般的にこぼれることについては賢明かもしれません。 それがそうであるように、それはパニックになります-それがレジスターを使い果たすとき、すべてをこぼします。
これのコンパイル時のコストは、主に、各制御フローエッジに関連付けられた情報の量が一定でないこと(レジスタの状態)であり、これにより、ベースラインコンパイラが持っている動的ストレージ割り当てのより広範な使用につながる可能性があります。はるかに避けた。 そしてもちろん、各結合(および他の場所)でその可変サイズの情報を処理することに関連するコストが発生します。 ただし、スピルコードを生成するにはレジスタの状態をトラバースする必要があるため、すでに一定ではないコストが発生します。また、概して、有効な値が少ない可能性があるため、これで問題ない場合もあります。 もちろん、regallocを賢く使うことは、高速キャッシュとooo実行により、最新のチップで報われるかもしれないし、報われないかもしれません...
より微妙なコストはコンパイラの保守性です...それはすでに非常に複雑であり、ワンパスであり、IRグラフを作成したり、ダイナミックメモリをまったく使用したりしないため、階層化や抽象化に耐性があります。
@RossTate
funclets / gotosに関しては、先日、funcletの仕様をざっと読みましたが、一見すると、ワンパスコンパイラには実際の問題はないように見えましたが、単純なregallocスキームでは問題はありませんでした。 しかし、より良いスキームでも問題ないかもしれません。ジョインポイントに到達する最初のエッジは、レジスタの割り当てが何であるかを決定するようになり、他のエッジは準拠する必要があります。
@ conrad-watt CGミーティングでおっしゃったように、マルチループがどのようになるかについての詳細を確認することに非常に興味があると思います。
@aardappelええ、人生は私に早く来ました、しかし私は次の会議でこれをするべきです。 @rossbergが最初のファンクレットのドラフトに応じて最初にスケッチしたので、このアイデアは私のものではないことを強調しておきます。
参考になるかもしれない1つのリファレンスは、少し古いものですが、 DJグラフを使用して既約ループを処理するために、ループのよく知られた概念を一般化し
これについてはCGで数回のディスカッションセッションがあり、要約とフォローアップドキュメントを作成しました。 長さのために、私はそれを別の要点にしました。
https://gist.github.com/conrad-watt/6a620cb8b7d8f0191296e3eb24dffdef
私は、2つのすぐに実行可能な質問(詳細についてはフォローアップセクションを参照)は次のとおりだと思います。
multiloop
からパフォーマンス面で利益を得る「ワイルド」プログラムを見つけることができますか? これらは、ソースプログラムに存在しない場合でも、LLVM変換によって還元不可能な制御フローが導入されるプログラムである可能性があります。multiloop
が最初にプロデューサー側で実装され、「Web」Wasm用のリンク/翻訳デプロイメントレイヤーが実装されている世界はありますか?フォローアップドキュメントで説明する例外処理の問題の結果については、おそらくもっと自由奔放な議論があります。もちろん、具体的なことを進めれば、セマンティックの詳細についての標準的なバイクシェディングもあります。
これらの議論は多少分岐する可能性があるため、それらのいくつかをfuncletsリポジトリの問題にスピンすることが適切な場合があります。
この問題の進展を見てとてもうれしく思います。 関係者の皆様、本当にありがとうございました!
現在苦しんでいて、マルチループからパフォーマンス面で利益を得る「ワイルド」プログラムを見つけることができますか? これらは、ソースプログラムに存在しない場合でも、LLVM変換によって還元不可能な制御フローが導入されるプログラムである可能性があります。
循環論法に対して少し注意したいのですが、現在パフォーマンスが悪いプログラムは、まさにこの理由で「実際に」発生する可能性が低くなります。
ほとんどのGoプログラムは多くの利益をもたらすはずだと思います。 Goコンパイラは、Goのコルーチンをサポートする効率的なコードを出力できるようにするために、WebAssemblyコルーチンまたはmultiloop
いずれかを必要とします。
プリコンパイルされた正規表現マッチャーは、他のプリコンパイルされたステートマシンとともに、多くの場合、既約の制御フローになります。 インターフェイスタイプの「融合」アルゴリズムが既約の制御フローをもたらすかどうかはわかりません。
FixIrreducibleControlFlow
とその友人によって引き起こされた非効率性は、大きなバイナリ全体での「1000カットによる死」の問題である可能性があります。goto <function_byte_offset>
だけです。 型署名のようなものは、エンジンがマルチループをすばやく検証する必要がある場合に役立ちますが、便利なツールであれば、放出するのに最大限便利になる可能性があります。LLVM(およびGoなど)が実際に最適な制御フロー(削減できない可能性がある)を放出しない限り、それから恩恵を受けるプログラムを見つけることは難しいことに同意します。
変更されたツールチェーンとVMでのテストが最適であることに同意します。 ただし、現在のwasmビルドを、最適な制御フローを備えたネイティブビルドと比較することはできます。 Not So Fastやその他の企業は、これをさまざまな方法(パフォーマンスカウンター、直接調査)で検討しており、還元不可能な制御フローが重要な要因であるとは考えていません。
具体的には、C / C ++にとって重要な要素であるとは考えていませんでした。 これは、既約の制御フローのパフォーマンスよりも、C / C ++に関係している可能性があります。 (正直なところわかりません。) @ neelanceには、Goにも同じことが当てはまらないと信じる理由があるよう
私の感覚では、この問題には複数の側面があり、複数の方向から取り組む価値があります。
まず、WebAssemblyの生成可能性に一般的な問題があるようです。 その多くは、効率的なタイプチェックとストリーミングコンパイルを備えたコンパクトなバイナリを持つというWebAssemblyの制約が原因です。 この問題は、生成が簡単であるが、理想的にはコードの複製と「消去可能な」命令/注釈の挿入によって「真の」WebAssemblyに変換できることが保証されている標準化された「pre」WebAssemblyを開発することで少なくとも部分的に対処できます。そのような翻訳を提供する少なくともいくつかのツールで。
次に、「pre」-WebAssemblyのどの機能を「true」WebAssemblyに直接組み込む価値があるかを検討できます。 「真の」WebAssemblyモジュールに歪められる前に分析できる「pre」-WebAssemblyモジュールがあるため、情報に基づいた方法でこれを行うことができます。
数年前、動的言語(https://github.com/ciao-lang/ciao)用の特定のバイトコードエミュレーターをWebAssemblyにコンパイルしようとしましたが、パフォーマンスは最適とはほど遠いものでした(ネイティブバージョンよりも10倍遅い場合があります)。 メインの実行ループには大きなバイトコードディスパッチスイッチが含まれており、エンジンは実際のハードウェアで実行されるように数十年にわたって微調整されており、ラベルとgotoを多用しています。 この種のソフトウェアは、既約の制御フローをサポートすることでメリットが得られるのでしょうか、それとも別の問題であるのでしょうか。 さらに調査する時間がありませんでしたが、状況が改善したことがわかっている場合は、もう一度やり直してください。 もちろん、他の言語のVMをwasmにコンパイルすることが主なユースケースではないことは理解していますが、特に、どこでも効率的に実行されるユニバーサルバイナリは、 wasm。 (この特定のトピックが他の問題で議論されている場合は、感謝と謝罪)
@jfmc私の理解では、プログラムが現実的であり(つまり、病理学的であるために考案されていない)、そのパフォーマンスを気にする場合、それは完全に有効なユースケースです。 WebAssemblyは、優れた汎用ターゲットになることを目指しています。 ですから、なぜこのような大幅な減速が見られたのかを理解できれば素晴らしいと思います。 それが制御フローの制限によるものである場合は、この説明で知っておくと非常に役立ちます。 それが何か他の原因である場合でも、WebAssemblyを一般的に改善する方法を知ることは有用です。
最も参考になるコメント
今後のGo1.11リリースでは、WebAssemblyが実験的にサポートされます。 これには、ゴルーチン、チャネルなどを含むGoのすべての機能の完全なサポートが含まれます。ただし、生成されたWebAssemblyのパフォーマンスは現在それほど良くありません。
これは主に、goto命令が欠落しているためです。 goto命令がなければ、すべての関数でトップレベルのループとジャンプテーブルを使用する必要がありました。 ゴルーチンを切り替えるときは、関数のさまざまなポイントで実行を再開できる必要があるため、リルーパーアルゴリズムを使用することはできません。 relooperはこれを支援できず、goto命令のみが支援できます。
WebAssemblyがGoのような言語をサポートできるようになったのは素晴らしいことです。 しかし、真にWebのアセンブリであるためには、WebAssemblyは他のアセンブリ言語と同等に強力である必要があります。 Goには、他の多くのプラットフォーム用に非常に効率的なアセンブリを出力できる高度なコンパイラがあります。 これが、主にWebAssemblyの制限であり、Goコンパイラの制限ではなく、このコンパイラを使用してWebの効率的なアセンブリを出力できないことを主張したい理由です。