先日、1つのオープンソースプロジェクトで1つのミスに取り組んでいたとき、同僚(同じ問題に並行して取り組んでいる)がそのようなコミット
[31a078bec7]をアップロードしたのを見ました。
/* - * Select the list item based on the index. Negative operand means - * end-based indexing (-2, ...), and -1 means out of range. + * Decode end-offset index values. */ - if (opnd < -1) { - index = opnd+1 + objc; - } else { - index = opnd; - } + index = opnd + (opnd <= TCL_INDEX_END)*(objc - 1 - TCL_INDEX_END); pcAdjustment = 5;
変更自体は正しいです(
TCL_INDEX_END
は定数定義
(-2)
)。
おおまかに言って、これは次のように展開されます(すべてint変数)。
index = opnd + cmp(opnd, (-2))==>(0 | 1) * (objc - 1 - (-2))
つまり、彼はそれによって条件付き遷移を1つ保存することを望んでいるかのようです。
そして、すべては何も無さそうに見えましたが、それでも私は、算術に偏りがあるような一見些細な「最適化」に驚いていました。
まず、この変更は、バイトコード(コンパイルされたTCL命令のJIT)の実行を担当するため、このプロジェクト(
TEBCresume
)の最も「メイン」な機能に関係しています。 このため、この関数は最大(約6,000行+プリミティブおよびマクロ)であり、プロジェクトコードベースで最も複雑なものの1つであり、複数の「goto」、実行、畳み込み/展開NREの「スタック」非再帰的評価)など など
つまり この関数への変更は、虫眼鏡や顕微鏡下でよく調べられます(わずかな変更でもこの関数のコード全体が上下逆になることがあるため)...
第二に、アクティビティのタイプごとに、アセンブラーの反射を調べてマイクロ秒からナノ秒の断片を絞り出すことにより、コードのコードを最適化する必要があります。すべてが非常に曖昧に発生することがよくあります。 少なくとも時々、そのような「節約」条件付きジャンプ構造を
if
または
if/else
にデプロイすると、結果のアセンブラコードと実行結果のパフォーマンスの最終比較の両方で改善が見られました。
実際、このすべてを書いたものに-私はこのトピックに触れてから、いくつかの統計を収集してから、例でそれがどのように起こるかを示したいと思いました。 したがって、記事の終わりにいくつかの世論調査...
完璧ではないにしても、非常に高いレベルに達している現代のコンパイラーでの最適化を忘れないでください。
別のことは、そこに最適化されたコンパイラーは、異常なまたは投機的な実行、コマンドレベルでの並列処理(ILP)など、独自の最適化を備えた最新のハードウェアで実行されると横に出ることがあるということです。
しかし、これはまったく異なる記事のトピックです。
明らかな理由により、元の関数ですべてを分析することはしません...
したがって、例として、2つの関数test1(算術の最適化)、およびtest2(1つの「if」を使用したフロー)、およびgccを例として使用するコンパイラーの異なるバージョンの両方の関数の結果のアセンブラーは、
-O2
および
-Ofast
テストされ
-Ofast
(実質的になし、およびトランク内で完全に変更なし):
c / c ++ |
---|
int test1(int opnd, int objc) { int index; index = opnd + (opnd <= (-2)) * (objc - 1 - (-2)); return index; }
| int test2(int opnd, int objc) { int index; if ((index = opnd) <= (-2)) { index += (objc - 1 - (-2)); } return index; }
|
x64、gcc 5.1: |
---|
;#test1(int,int) xor eax, eax cmp edi, -1 setl al add esi, 1 imul esi, eax lea eax, [rsi+rdi] ret
| ;#test2(int,int) lea edx, [rdi+1+rsi] mov eax, edi cmp edi, -2 cmovle eax, edx ret
|
x64、gcc 7.3: |
---|
;#test1(int,int) xor eax, eax cmp edi, -1 setl al add esi, 1 imul eax, esi add eax, edi ret
| ;#test2(int,int) lea ecx, [rdi+1+rsi] mov eax, edi cmp edi, -1 cmovl eax, ecx ret
|
x64、gcc(トランク): |
---|
;#test1(int,int) add esi, 1 mov eax, 0 cmp edi, -1 cmovge esi, eax lea eax, [rsi+rdi] ret
| ;#test2(int,int) mov eax, edi cmp edi, -1 lea ecx, [rdi+1+rsi] cmovl eax, ecx ret
|
x86、gcc 5.1: |
---|
;#test1(int,int) mov ecx, [esp+4] mov edx, [esp+8] xor eax, eax cmp ecx, -1 setl al add edx, 1 imul eax, edx add eax, ecx ret
| ;#test2(int,int) mov eax, [esp+4] mov edx, [esp+8] lea edx, [eax+1+edx] cmp eax, -2 cmovle eax, edx ret
|
x86、gcc 7.3: |
---|
;#test1(int,int) mov ecx, [esp+4] mov edx, [esp+8] xor eax, eax cmp ecx, -1 setl al add edx, 1 imul eax, edx add eax, ecx ret
| ;#test2(int,int) mov eax, [esp+4] mov edx, [esp+8] lea edx, [eax+1+edx] cmp eax, -1 cmovl eax, edx ret
|
x86、gcc(トランク): |
---|
;#test1(int,int) mov edx, [esp+4] mov eax, [esp+8] mov ecx, 0 add eax, 1 cmp edx, -1 cmovge eax, ecx add eax, edx ret
| ;#test2(int,int) mov eax, [esp+4] mov edx, [esp+8] cmp eax, -1 lea edx, [eax+1+edx] cmovl eax, edx ret
|
より明確に(バックライトなどを使用)、これは
ここまたは
ここで見ることができ
ます 。
バランスにあるもの:
- すべての場合(そして最大の最適化でも)
test1
は良くない、またはtest2
失う test2
は、たとえば条件付きのcmovl
を使用するだけのように、コンパイラによって実装され、条件付きジャンプはまったくありません。- ネイティブの
test2
も読みやすい test2
より短く、ILP / SEPにより便利な場所もあり、CPUキャッシュ(少なくとも命令キャッシュ)の侵食が少なくなります。
ここで、ところで、コンパイラの開発のいくつかの進歩がはっきりと見えます。
さて、関数全体の完全な形で、コンパイラーはフロー全体をわずかに変更しましたが、一部の人為的なパフォーマンステストでは1から2パーセントに低下しました(他のコンパイラー/システム/プラットフォームなどでは異なる場合があります)。
コンパイルされたTCLコードの実行(寄生負荷なし)の一般的なテストでは、わずかな低下(割合の割合)が示されますが、これは単に真空の単位体積のエネルギーの変動に関連付けられます。
さて、なぜあなたはあなたの心の中でそのような「ブラインド」最適化を行う必要がないように思えますか:
- 多数の数学と実際のプロセスの流れが混ざり合っており、たとえばコンパイラーでの解析が複雑になっている
- コンパイルプロセスで最適化する場合、数学では、分岐は同じフローツリーおよびインラインプロセスよりも少し弱く、制御オーバーフロー、暗黙的な動作などが必要です。 (これは歴史的に、大まかに言って、数学はそれを最適化することにわずかに焦点を合わせています)。
- ソースコードが暗黙的になり、少なくともレビューと分析が遅くなります(読みにくい場合もあります)。
このリストは無期限に継続できます。たとえば、特定のプロセッサ(コンパイラでサポートされている場合)のソースをコンパイルする場合、明示的な必須動作により、コンパイラ(またはその新しいバージョン)が最適なシナリオを見つける可能性が高くなります(たとえば、新しいプロセッサ命令を使用したり、「重複」 »すべてのコードをひっくり返すなど
最適化する必要はまったくありませんが、これを行う場合は、虫眼鏡で武装し、ネイティブコンパイル結果を確認し、実行速度の測定、パフォーマンスのプロファイル(特定の関数とコード全体の両方)などを行います。
私はしばしばバイトコードを改善するように見えるいくつかの最適化を見ました(より少ない命令とより短い命令、最適なレジスタの読み込みなど)。マルチスレッドおよび/または並列浮遊負荷の下で実際のテストの実行を数パーセント遅くします。
PSすべての女の子の幸せな休日!
投票のシムについては...