2017-01-10 3 views
1

レジスタにある既存の値を一時的に保存する場合は、最新のコンパイラ(少なくとも私が経験したコンパイラ)すべてがPUSHおよびPOP命令を実行します。しかし、もし利用可能であれば、別のレジスタにデータを保存してみてはいかがですか?"プッシュ" "ポップ"または "移動"?

既存の値を一時的に保存する場所はどこですか。スタックまたは登録?結局のところ

MOV ECX,16 
LOOP: 
MOV ESI,ECX ;Value saved to ESI register  
...  ;Assume that here's some code that must uses ECX register 
MOV ECX,ESI ;Value returned to ECX register 
SUB ECX,1 
JNZ LOOP 

、上記のコードの一つであり、より良い、なぜ:

MOV ECX,16 
LOOP: 
PUSH ECX ;Value saved to stack  
...  ;Assume that here's some code that must uses ECX register 
POP ECX  ;Value released from stack 
SUB ECX,1 
JNZ LOOP 

今すぐ2STコードを考えてみます。

は、以下の第一のコードを考えてみましょうか?

個人的には、最初のコードはPUSHとPOPが1バイトしかかからず、MOVが2を取るのでサイズが優れていると思います。レジスタ間のデータ移動がメモリアクセスより高速であるため、2番目のコードは速度が向上します。

+5

スタック上の値を使用して、それらが占有するレジスタを使用できるようにします。なぜ彼らは他のレジスタにそれらを移動しないのですか?おそらく、他のレジスタもいくつかの値に必要とされるためです。 – fuz

+2

ループ内でESIレジスタが空いている場合は、カウンタをESIに入れてシャッフルしない方がよいでしょう。あなたのコンパイラがスマートなら、それはそれを知っているでしょう。結論:あなたはダンプコンパイラを持っているか、ループ内でESIがフリーではないことを知っているか、他の空きレジスタがないことを知っています。その場合、PUSH/POPの組み合わせはひどいものではありません。 –

+0

* "最近のコンパイラ(少なくとも私が経験したもの)はPUSHとPOP命令を実行する" * ...これはかなり偽の主張であり、 'gcc'や' clang'を試してみると、ループ内に余分なレジスタを置いておけば、 '[ebp-ofs]/[esp + ofs]'というローカル変数を使うことになるでしょう。私は、これらの2つのPUSH/POPを生成するC/C++ソースを見たいと思っています。その後、これらの2つの基本的なコンパイラも基本的に唯一のコンパイラなので、何を確認したのかは分かりません。 – Ped7g

答えて

1

レジスタの使用は少し高速ですが、使用可能なレジスタを追跡する必要があり、レジスタを使い果たすことがあります。また、このメソッドを再帰的に使用することはできません。さらに、サブルーチンを呼び出すためにINTまたはCALLを使用すると、一部のレジスタはゴミ箱になります。

スタック(POPおよびPUSH)の使用は、スタックスペースが不足している限り、必要な回数だけ使用できます。さらに、再帰的ロジックもサポートされています。慣例により、サブルーチンはスタックの独自の部分を確保し、それを以前の状態に戻す必要があるため(つまり、RET命令が失敗するため)、スタックをINTまたはCALLで安全に使用できます。

1

スピードについて考えるときは、常に比例感を覚えておく必要があります。それらの間に実行される命令の数に比べ

関数呼び出しの他の機能をコンパイルされる場合、それら pushpop命令は些細であってもよい、 。

コンパイラライターは、この種のケースでは非常に一般的であることを知っていますが、penny-wise and pound-foolishであってはなりません。

1

PUSHとPOPを使用すると、少なくとも1つのレジスタを保存できます。限られた使用可能なレジスタで作業している場合、これは重要です。一方、時にはMOVを使用する方が速度が良いこともありますが、一時的な記憶域として使用されているレジスタも覚えておく必要があります。後で処理する必要があるいくつかの値を保存したい場合、これは難しいでしょう。

1

これは意味があります。しかし、私は最も単純な答えは、他のすべてのレジスタが使用されていると思います。他のレジスタを使用するには、スタックにプッシュする必要があります。

コンパイラは十分にスマートです。コンパイラのレジスタにあるものを追跡することはややこしいことですが、それは問題ではありません。一般的に必ずしもx86とは言えないが、x86よりも多くのレジスタを持っているときは、(呼び出し規約で)入力に使用されるいくつかのレジスタを持つことになり、ごみ箱に入れることができるレジスタがあなたが最初にそれらを保存する必要がありますごみ箱を入力するかどうか、いくつかを入力します。命令セットには特殊レジスタがあり、自動インクリメントにはこのレジスタを、レジスタ間接には命令を使用する必要があります。

たとえば、入力と切り捨て可能なレジスタが同じセットである場合など、コンパイラに腕のコードを生成させるのは簡単ではありませんが、別の関数を呼び出して呼び出し関数を作成するとそれは返品後に使用するものを保存する必要があります:

unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(x)+x); 
} 
00000000 <fun>: 
    0: e92d4010 push {r4, lr} 
    4: e1a04000 mov r4, r0 
    8: ebfffffe bl 0 <more_fun> 
    c: e0840000 add r0, r4, r0 
    10: e8bd4010 pop {r4, lr} 
    14: e12fff1e bx lr 

私はそれが些細なことだと言った。あなたの議論を後方に使うために、なぜ彼らはスタック上のr0をプッシュして後でポップするのですか?なぜr4を押しますか? r0ないしr3は入力に使用され、揮発性であり、r0はそれが適合するときのリターンレジスタであり、r4はほとんどすべての方法で保存しなければならない(1つは例外と考える)。

r4は呼び出し元や呼び出し元によって使用されていると見なされますが、呼び出し規約ではそれをゴミ箱に保存できないように指示していますので、使用すると仮定する必要があります。あなたはr0〜r3をゴミ箱に入れることができますが、被呼者がそれらをゴミ箱に入れることはできませんので、受信した値xを取ってそれを使う(渡す)必要があります。彼らは両方のことをしました、 "移動で別のレジスタを使用しました"が、そうするために、彼らは他のレジスタを保存しました。

この場合のスタックへのr4の保存は非常に明白です。なぜなら、あなたは常にスタックを64ビットのチャンクで使用することを望んでいますので、一度に2つのレジスタを理想的に少なくとも64ビットの境界線に整列させておくと、とにかくlrを保存しなければならないので、そうしないと何か他のものを押し込むことになります。この場合、r4の節約は無料です。 r0を保存すると同時にそれを使用します。 r4またはr5または上記の何かが良い選択です。

BTWは上記のx86コンパイラのようです。

0000000000000000 <fun>: 
    0: 53      push %rbx 
    1: 89 fb     mov %edi,%ebx 
    3: e8 00 00 00 00   callq 8 <fun+0x8> 
    8: 01 d8     add %ebx,%eax 
    a: 5b      pop %rbx 
    b: c3      retq 

彼らは維持する必要がいけない何かを押してそれらのデモンストレーション:

unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(x)+1); 
} 
00000000 <fun>: 
    0: e92d4010 push {r4, lr} 
    4: ebfffffe bl 0 <more_fun> 
    8: e8bd4010 pop {r4, lr} 
    c: e2800001 add r0, r0, #1 
    10: e12fff1e bx lr 

R4を保存する理由はない、この場合には、R4が選ばれたので、彼らはただ、整列スタックを作るために、いくつかのレジスタを必要とこのコンパイラのいくつかのバージョンでは、r3やその他のレジスタが使用されています。

コンパイラとオプティマイザを書く人間を覚えておいてください。なぜ、これがなぜ人間や人間にとって本当に質問なのですか?これは単純な作業ではありませんが、合理的なサイズの関数やプロジェクトを作成し、コンパイラの出力を調整して改善することは困難ではありません。もちろん、美しさは見る者の目の前にあり、改善の定義はもう一つの定義が悪化することです。 1つの命令ミックスは、プログラムのサイズ基準では「より良い」、命令やバイトを多く使用することもあれば使用しないこともありますが、実行時間が短く、理想的に実行する命令のコストでメモリアクセスが少なくなるより速いなど

汎用レジスタは数百もありますが、私たちが毎日使っている製品のほとんどにはそのようなものがありません。そのため、一般的には、飛行中に非常に多くの変数を持つ関数やコードを作ることができますあなたはスタックの中間関数をオフに保存しなければならない関数で。だから、関数の最初と最後にいくつかのレジスタを保存するだけでは、中途関数が必要な作業レジスタの数があなたが持っているより多くのレジスタであれば、より多くの作業レジスタを中間関数として与えることはできません。実際には、あまりにも多くのレジスタを必要としないように最適化しないコードを書くことができるようになるには練習が必要ですが、コンパイラがどのように出力を調べるかを見てから、上記のような簡単な関数を書くことができますレジスタ中間機能の最適化または強制保存など

コンパイラがやや正気にならないようにするためには、呼び出し規約が必要です。作成者はコードを作成して管理することから悪夢にならず、コンパイラは邪魔にならないようにします。また、呼び出し規約では、入力レジスタと出力レジスタ、および揮発性レジスタと、保持しなければならないレジスタを明確に定義しようとしています。

unsigned int fun (unsigned int x, unsigned int y, unsigned int z) 
{ 
    unsigned int a; 

    a=x<<y; 
    a+=(y<<z); 
    a+=x+y+z; 
    return(a); 
} 
00000000 <fun>: 
    0: e0813002 add r3, r1, r2 
    4: e0833000 add r3, r3, r0 
    8: e0832211 add r2, r3, r1, lsl r2 
    c: e0820110 add r0, r2, r0, lsl r1 
    10: e12fff1e bx lr 

これで数秒しか費やされませんでしたが、それをもっと重視することができました。私は4つの変数を持っていた過去4つのレジスタ合計をプッシュしなかった。そして、コンパイラは、依存関係が解消されたときに、必要に応じてr0-r3をゴミ箱に自由に入れられるように、関数を呼び出さなかった。だから、私は一時的なストレージを作成するためにr4を保存する必要はありませんでした、それはちょうど実行順序を最適化したスタックを使用する必要はありませんでした。たとえば、z2変数を解放して、後でr2を中間変数aのインスタンスの1つは何かに等しい。 5つ目のレジスタを書き込むのではなく、4つのレジスタに保存しておきます。

私が自分のコードで創造力を発揮し、関数呼び出しを追加した場合、もっと多くのレジスタを焼くことができました。この最後のケースでもコンパイラは何の問題もない何がどこにあるのか、そしてコンパイラを使って遊んだときに、あなたがコードを書いたのと同じ順番で、同じレジスタに高レベルの言語変数をそのまま残しておく必要はありませんしかし、レジスタのほんの一部が不安定であるとみなされた場合でも、呼び出し規約の慈悲のもとにいます。コード内の特定の時間に関数から関数を呼び出すと、その関数を保持する必要がありますコンテンツを長期保存として使用することはできません。また、揮発性でないものはすでに消費されているとみなされているので、それらを使用するには保存する必要があります。パフォーマンス上の問題がありますが、その場でスタックに保存するためには(サイズ、スピードなど)コストがかかりますか、命令を減らしたり、見えなくしたり、クロックを少なくしたりする方法で前面を維持できますか?別々の、より効率的でない転送中間関数よりも大きな転送?

私はこれを今7回言いましたが、最終行はそのコンパイラ(バージョン)とターゲット(およびコマンドラインオプション/デフォルト)の呼び出し規約です。揮発性レジスタ(ハードウェア/ ISAのものではなく、汎用レジスタの任意の呼び出し規約)があり、他の関数を呼び出さない場合は、使いやすく、高価なスタック(メモリ)トランザクションを節約できます。あなたが誰かに電話をしている場合、彼らは彼らが自由になることができないように、あなたのコードに依存して彼らがゴミ箱に捨てることができます。不揮発性レジスタは呼び出し元によって消費されると見なされるため、スタック操作を使用するためにスタック操作を書き込む必要があります。スタック操作は自由に使用できません。そして、スタック、プッシュ、ポップ、ムービーをいつ、どこで使うかはパフォーマンスになります。同じコンベンションを使用しても同じコードを生成するコンパイラは2つありませんが、テスト関数を作成し、コンパイルして出力を検査し、そこを微調整してその前後をナビゲートすることはやや簡単です(コンパイラ、バージョンとターゲット、規約とコマンドラインオプション)オプティマイザ

+0

"人間がオプティマイザを書くのを覚えています":右。最適なコンフィギュレーションを探すアルゴリズム(レジスタアロケータなど)を介して行うので、おそらくそれ自体でも結果を予測することはできません。 –

+0

私はそれが意味することは、人間がレジスタにコピーすることを選択するコードを書いていなければ、コンパイラはレジスタにコピーすることはできません。上記の最適化のいずれも、人間がそれを知り、思考し、それを実装することなしには起こりませんでした。今度は実装が試行しなければならない場合や、どのレジスタを使っていくつかのパラメータに基づいて選択するアルゴリズムである可能性が高いことを確認してください。 –

+0

異なる人間によって書かれた異なるコンパイラは、部分的に異なる人間のために、アルゴリズムの違いのために部分的に異なる命令ミックスを使用する傾向があることがわかります。あるプロセッサをテストすると、あるコンパイラが命令の一部を使用するコードを持っていないことが判明しました。インラインアセンブリ以外のものを生成したことはありませんでした。他の人がそれらを使用するケースを持っていたところ。あるコンパイラでは、他のコンパイラでは(同じテストコードでコンパイラを単に切り替えるだけでチップのバグが見つかった)コンパイラでは決してシーケンスが発生しませんでした。 –

0

数十年のコード生成スペシャリストの作業に基づいて最適化コンパイラの作業を信頼してください。

これらのレジスタは、使用可能なレジスタをすべて満たすだけでなく、必要に応じてさまざまなオプションを比較してスタックに拡張されます。また、後で再利用するための値の保存と値の再計算との間のトレードオフについても気にしています。

「レジスタ対スタック」という単一のルールはありませんが、プロセッサの特質を考慮してグローバルな最適化の問題です。そして、一般に、あなたの「ベストプラクティス」の基準に依存するため、単一の「ベストソリューション」はありません。

非常に創造的な回避策が見つかった場合(またはあなただけが知っているデータプロパティを利用する場合)を除いて、コンパイラを倒すことはできません。

関連する問題