アセンブラを使用して、Cをより深く理解します。 パート3

3番目の記事では、条件の分析を続けます。 前回は、最適化されたif-elseバージョンを検討しませんでした。



おそらく、このために、たとえばコンパイラがここにある理由など、公正な疑問が生じました。



int x = 10; scanf("%i", &x); int c = 0; if (x < 4) {     c = 3; } else {     c = 2; } return c;
      
      





1つではなく、2つの遷移を持つ生成コード。



確かに、ここで私たち自身が少し奇妙なコードを書いたのは、明らかに、このように書かれていたかもしれないからです:



 int x = 10; scanf("%i", &x); int c = 2; if (x < 4) {    c = 3; } return c;
      
      





コンパイラではなく、なぜ他に必要なのかをプログラマに尋ねるべきです。 しかし、最初のオプションの最適化されたバージョンを見ると、コンパイラがそれほど愚かではないことがわかります。



  sub esp, 12 mov dword ptr [esp + 8], 10 sub esp, 8 lea eax, [esp + 16] push eax push .L.str call scanf add esp, 16 xor eax, eax ;   eax cmp dword ptr [esp + 8], 4 ;     4 setl al ;   ,   al  1 or eax, 2 ;    2 add esp, 12 ret .L.str: .asciz "%i"
      
      





遷移がまったくないことに注意してください。 そして、clangが私たちの状態を理解する方法をすでに見ました。 2番目のオプションでは、まったく同じ最適化されたコードが生成されます。



これを思い出してみましょう:



 int main(void) { int a = -1; scanf("%i", &a); if (a++ < 0 || a++ > 5) { a++; } else { a+=2; } return a; }
      
      





前回は少し怖かったですか? 最適化されたバージョンは、コードの量とラベルの数をわずかに削減できます(scanfはスキップします)。



  mov ecx, dword ptr [esp + 8] lea eax, [ecx + 1] test ecx, ecx mov dword ptr [esp + 8], eax js .LBB0_2 lea eax, [ecx + 2] cmp ecx, 5 mov dword ptr [esp + 8], eax jl .LBB0_3 .LBB0_2: inc eax add esp, 12 ret .LBB0_3: add ecx, 4 mov eax, ecx add esp, 12 ret
      
      





ラベル.LBB0_2までは、コードは条件自体に対応しています。 最適化されたバージョンでは、ロジックは「第2オペランド」または「第1オペランドがtrueの場合は計算されない」に違反しないことに注意してください。 まあ、それでも、彼女は壊れていました...



ただし、最適化されていないバージョンとは大きな違いがあります。 誰もが何が起こっているのか理解できるようにコードを見ていきましょう。



最初の行で、ecxは変数「a」の値を取得します。

eaxの2行目は+ 1を取得します

次はゼロとの比較です。これにより、数値が負の場合、js命令は数値2のラベルに移動します。

4行目では、変数 "a"はeaxから+ 1を取得します。



最適化されていないバージョンでは、変数の値を再度取得してeaxに入れましたが、今は古い値を取得し、まだecxにあります。 次に、+ 2を実行してeaxで保存します。



そして、インクリメントの前の数値を5と比較します。これにより、jl命令は、値が小さい場合に3番目のラベルにジャンプし、条件に該当しません。



「clangは、最適化されたバージョンと最適化されていないバージョンのプログラムで2つの異なる結果を生成しますか?」



いいえ、問題は、else inが5以下で発生し、以下では発生しないことです。 コンパイラーは、変数が5の場合の疑わしい結果を考慮して、条件を作成しました。 その結果、追加のメモリアクセスを節約しました。



最適化されたバージョン:

5> = 5 => 5 + 3

最適化されていない

7> 5 => 7 + 1



それ以外の場合、最適化されたバージョンでは、結果に4を追加してeaxにドロップするだけです。 これらは奇跡です。



最適化されていないバージョンのgccと最適化されたclangを比較するのはなぜですか? まあ、gccは同じことをしますが、あまりきれいではありません。 また、clangでは、コードは一貫してコンパクトに見えます。



VS2015のコンパイラは、完全な最適化で1つのマークしか残していないことに注意してください。



  mov eax, DWORD PTR _a$[ebp] ;a → eax add esp, 8 mov ecx, eax inc eax test ecx, ecx js SHORT $LN4@main ; if (a<0) mov ecx, eax inc eax cmp ecx, 5 jg SHORT $LN4@main ; if (a>5) add eax, 2 ;else a+=2 ... $LN4@main: ... inc eax ;a++ in if ...
      
      





間にあるのは私たちのものには当てはまらないコードなので、削除しました。 ご覧のとおり、条件は他をスキップするように設計されているため、ラベルは1つだけです。 しかし、コードを後から追加する価値があり、2つのラベルもあります。 この場合、最初に1つの値を割り当てるという事実により、コードは最適化されていないバージョンに似ていますが、条件が満たされると、値は上書きされます。



スイッチ



さて、switchステートメントはどうですか? ifsのバッテリーがこの「美しい」演算子とどのように異なるか。 次のコードの例を見てみましょう。



 char c = getchar(); int a; if (c == 'q') { a = 0; } else if (c == 'w') { a = 1; } else if (c == 'e') { a = 2; } else { a = 4; } return a;
      
      





およびその代替:



 char c = getchar(); int a; switch(c) { case 'q': a = 0; break; case 'w': a = 1; break; case 'e': a = 2; break; default: a = 4; } return a;
      
      





最適化されていないバージョンについては詳細に検討しません。 違いがあるとしても、それが最小限であることは誰もが理解していると思います。 しかし、最終的にさらに多くのコードが生成されるのはスイッチ用であることは面白いです(gcc 7.2):



 movsx eax, BYTE PTR [ebp-13] ;            cmp eax, 113 je .L3 cmp eax, 119 je .L4 cmp eax, 101 je .L5 jmp .L8
      
      





ifの場合、すでにコードを提示する必要があります:比較-アクション-遷移。 そして、ここでは、アクションが後まで延期されることがわかります。



スイッチ用のgccの最適化されたバージョンでは、コードは短くなりますが、clangでは違いはまったくありません。 それでは、gccのスイッチのみを見てみましょう。 しかし、最初に、このコードのスイッチを完全に削除する方法を考えてください。



だから、あなたが思いついたら、スポイラーを開いて見てください:



asmコード
  push DWORD PTR stdin call _IO_getc lea edx, [eax-101] add esp, 16 mov eax, 4 cmp dl, 18 ja .L1 movzx edx, dl mov eax, DWORD PTR CSWTCH.3[0+edx*4] .L1: mov ecx, DWORD PTR [ebp-4] leave lea esp, [ecx-4] ret CSWTCH.3: .long 2 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 0 .long 4 .long 4 .long 4 .long 4 .long 4 .long 1
      
      





アイデアは非常にシンプルです。文字コードと「a」の合計数を一致させます。 これは、結果からスイッチから最小の文字コードを引くことで実現できます。この場合、「e」または101です。



それから、eax 4を入れます。

次に、edxと18の内容を比較します(edxの場合のみdlであるalのように、このレジスタから下位バイトのみを取得します)。 値が(ja)より大きい場合は、ラベル.L1に移動します。 質問が発生した場合:「なぜ18?」、次に「w」-101を実行します。



ここで、結果が0〜18の場合、式は機能します。moveax、DWORD PTR CSWTCH.3 [0 + edx * 4]。これは、edxの数値に基づいて結果を取得することを意味します。メモリ(4バイト)。



0( 'e')の場合、結果は2になります

12( 'q')の場合、結果は0になります。

18( 'w')の場合、結果は1になります

それ以外の場合は、4になります。



私たちは人生から非常に離婚した例がありますが、それはありそうにないので、一般的に誰かがスイッチを使用しました。 何らかのアクションが起こっている場合はどうでしょうか? 驚くかもしれませんが、全体的には何も変わっていません。 この例を考えてみましょう:



 #include <stdio.h> #include <math.h> int main(void) { start:    char c = getchar();       switch (c)    {    case 'T':    case 't': printf("Talk\n"); break;    case 'W':    case 'w': printf("%f\n", sin(0)); break;    case 'Q':    case 'q': return 0;    default:        printf("wrong command\n");    }    goto start;   }
      
      





asmコード
 .LC0: .string "Talk" .LC2: .string "%f\n" .LC3: .string "wrong command" main: ... .L2: ;start sub esp, 12 push DWORD PTR stdin call _IO_getc sub eax, 81 add esp, 16 cmp al, 38 ja .L3 movzx eax, al jmp [DWORD PTR .L5[0+eax*4]] .L5: .long .L9 .long .L3 .long .L3 .long .L6 .long .L3 .long .L3 .long .L7 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L9 .long .L3 .long .L3 .long .L6 .long .L3 .long .L3 .long .L7 .L7: ;sin sub esp, 4 push 0 push 0 push OFFSET FLAT:.LC2 call printf add esp, 16 jmp .L2 .L6: ;Talk sub esp, 12 push OFFSET FLAT:.LC0 call puts add esp, 16 jmp .L2 .L9: ;return 0; mov ecx, DWORD PTR [ebp-4] xor eax, eax leave lea esp, [ecx-4] ret .L3: ;default sub esp, 12 push OFFSET FLAT:.LC3 call puts add esp, 16 jmp .L2
      
      





コードがたくさんあるので、主なことに集中してください。 これで、eaxで数値を移動する代わりに、特定のラベルに移行します。 しかし、アイデア自体は変わりません。



おわりに



ようやく条件に対処したようです。 誰かがスイッチについて疑っていた場合、今ではあなたはそれを恐れることはできません。 次の記事では、サイクルについて説明します。



前の記事



All Articles