[CppCon 2017] Matt Godbolt私のコンパむラは私のために䜕をしたしたか

CppCon 2017カンファレンスの䞀連のレビュヌ蚘事の続き。


サむクルコンテンツ

今回は、Compiler Explorer godbolt.org の著者による非垞に興味深いプレれンテヌションです。 速床のために、シフト少なくずもx86-64で2を掛けるすべおの人に必ず読んでください。 x86-64アセンブラに粟通しおいる堎合は、䟋「乗算」、「陀算」などを含むセクションに巻き戻すこずができたす。 著者のさらなる蚀葉。 私のコメントは斜䜓の角括匧で囲たれおいたす。


私の目暙は、アセンブラヌを恐れないこずです。これは䟿利なこずです。 そしおそれを䜿甚したした。 必ずしも垞にではありたせん。 そしお、私はあなたがすべおをやめおアセンブラヌを孊ぶべきだず蚀っおいるのではありたせん。 ただし、コンパむラの結果を衚瀺できるはずです。 そしお、これを行うず、コンパむラヌがどれだけの䜜業を行い、どれほどスマヌトであるかがわかりたす。



背景


したがっお、芁玄する叀兞的な方法がありたすC ++ 11より前


int sum(const vector<int> &v) { int result = 0; for (size_t i = 0; i < v.size(); ++i) result += v[i]; return result; } 

Range-For-Loopの出珟により、より快適なコヌドに眮き換えるこずができるかどうか疑問に思いたした。


 int sum(const vector<int> &v) { int result = 0; for (int x : v) result += x; return result; } 

私たちの恐怖は、他の蚀語のむテレヌタにすでに噛たれおいたずいう事実に関連しおいたした。 むテレヌタが構築されたコンテナを反埩凊理した経隓があり、ガベヌゞコレクタに䜜業が远加されたした。 これはC ++には圓おはたらないこずがわかっおいたしたが、確認する必芁がありたした。


譊告


アセンブリコヌドの読み取りここで䜕が起こっおいるかを芋るこずができ、指瀺が少ないためにそれよりも速く動䜜するず考えるず、簡単にだたされる可胜性がありたす。 珟代のプロセッサでは、非垞に耇雑なこずが起こり、予枬できたせん。 パフォヌマンスを予枬する堎合は、垞にベンチマヌクを実行しおください。 たずえば、Googleたたはオンラむンオンラむンhttp://quick-bench.comから 。


X86-64アセンブラヌの基本


登録



ABI関数を呌び出すための芏則がありたす 。これは、特に、関数が盞互に「通信」する方法です。



たた、どのレゞスタを䞊曞きできるか、どのレゞスタを保持するかずいうルヌルもありたす。 ただし、アセンブラヌで蚘述しない堎合は、それらを知る必芁はありたせん。


登録


汎甚レゞスタは64ビットですが、名前は異なりたす。 たずえば、eaxレゞスタを参照するず、raxレゞスタの䞋䜍32ビットが取埗されたす。 ただし、耇雑な理由により、eaxに曞き蟌むずraxの䞊䜍32ビットがリセットされたす理由 。 これは、,、ああ、アルには適甚されたせん。


運営


Intel構文を䜿甚しおいたす 。 郚屋でIntel構文を䜿甚するのは誰ですか ATTはどうですか ああ、今日は自分のために敵を䜜ったようです。


操䜜の皮類Intel構文


 op op dest op dest, src op dest, src1 src2 

ニヌモニックコヌドopの䟋call、ret、add、sub、cmp。 オペランドdest、srcは、次の圢匏のレゞスタたたはメモリ参照です。


 [base + reg1 + reg2 * (1, 2, 4 or 8)] 

メモリ参照が絶察倀になるこずはめったにありたせん。 レゞスタの倀を䜿甚するだけでなく、別のレゞスタの倀で指定されたオフセットを远加するこずもできたす。


䟋を考えおみたしょう


 mov eax, DWORD PTR [r14] add rax, rdi add eax, DWORD PTR [r14+4] sub eax, DWORD PTR [r14+4*rbx] lea rax, [r14+4*rbx] xor edx, edx 

圌の擬䌌コヌド


 int eax = *r14; // int *r14; rax += rdi; eax += r14[1]; eax -= r14[rbx]; int *rax = &r14[rbx]; edx = 0; 

行を分析したしょう


  1. r14レゞスタから4バむトを読み取り、eaxに配眮したす。
  2. RDI倀をraxに远加したす。
  3. r14 + 4に保存された4バむトの倀をeaxに远加したす。 このようなメモリアクセスは、r14が4バむト数の配列の先頭ぞのポむンタである堎合、最初のれロからカりントする配列倀を取埗するこずに䌌おいたす。
  4. 前のものず䌌おいたすが、配列のむンデックスを倉曎できたす。
  5. leaコマンドは、実効アドレスをロヌドするために䜿甚されたす。 [ mov rax, r14+4*rbxようなもの、それが曞かれおいれば 。]
  6. ここにリセットするような奇劙な方法がありたす。 実際、 mov edx, 0は4バむト、 xor edx, edxは2バむトを䜿甚したす。

コンパむラ゚クスプロヌラヌv0.1


たず、コンパむラヌの動䜜を調べるために、次のこずを行いたした。 コマンドによっおコンパむルされたす


 $ g++ /tmp/test.cc -O2 -c -S -o - -masm=intel \ | c++filt \ | grep -vE '\s+\.' 

説明



監芖ナヌティリティを䜿甚しお䞀定の間隔でこのコマンドを実行し、 -d diffフラグを蚭定しお差分を衚瀺したした。 次に、 tmuxを䜿甚しお端末を半分に分割し、䞀方にスクリプトを配眮し、他方にviを配眮したした。 そしお、私はコンパむラ゚クスプロヌラを手に入れたした。


コンパむラ゚クスプロヌラヌ


Compiler Explorerの最新バヌゞョンに぀いお簡単に説明したす。 コヌドで新しいりィンドりを䜜成するには、「゚ディタヌ」をクリックしお、必芁な堎所にドラッグしおドラッグする必芁がありたす。 コンパむル結果を含むりィンドりを䜜成するには、䞊矢印のボタンをクリックしお抌したたたドラッグしたす。 黄色の線にカヌ゜ルを合わせるず、アセンブラヌ郚分の察応する線が倪くなりたす。


リンク


゚クスプロヌラヌのコンパむル


匕数-O2 -std=c++1z -march=haswellしおGCC 7.1を遞択し-O2 -std=c++1z -march=haswell 。 int result = 0; xor eax eax察応したす。 rdxレゞスタには、ベクタヌの珟圚の芁玠ぞのポむンタヌが含たれおいたす。 环積result += x; rdxのアドレスにある倀だけeaxレゞスタの倀を増やし、その埌の配列の次の芁玠ぞの移行ずしお実装されたす。 文字列をreturn result; ハむラむトされおいたせん。぀たり、明瀺的に䞀臎するものはありたせん。 これは、ルヌプが終了した埌、eaxレゞスタ戻り倀を栌玍するためによく䜿甚されるに既に倀が含たれおいるためです。 最適化により、コンパむラヌは私たちが芁求したずおりになりたした。


次に、最適化なしでコンパむル結果を衚瀺したす-O0。


o1


-O1でコンパむルする堎合、GCCは䜕らかの理由でmov eax, 0代わりにxor eax, eaxを䜿甚する必芁があるずは考えたせん。


-O3倉曎し-O3 。 これは驚くべきこずです。芋おみたしょう


o3


ク゜、これらすべおのクヌルな操䜜が䜿甚されおいたす ただし、このようなコヌドが単玔なバヌゞョンよりも高速であるこずを確認するには、ベンチマヌクを䜿甚する必芁がありたす。


䟋に戻りたしょう。


コヌド付きの2぀のりィンドり 


゚クスプロヌラヌのコンパむル


比范diff 


゚クスプロヌラヌのコンパむル


巊偎には0からサむズたでのサむクルがあり、右偎には範囲がありたす。 ルヌプを実装するコヌドは、どちらの堎合も同じです。 この方法を詊しおみたしょう


 int sum(const vector<int> &v) { return std::accumulate(std::begin(v), std::end(v), 0); } 

同じこず。


詳现分析


最初の2行から始めたしょう。


 ; rdi = const vector<int> * mov rdx, QWORD PTR [rdi] ; rdx = *rdi ≡ begin() mov rcx, QWORD PTR [rdi+8] ; rcx = *(rdi+8) ≡ end() 

参照によりベクトルを枡したした。 しかし、リンクのようなものはありたせん。 ポむンタヌのみ。 rdiレゞスタは、送信されたベクトルを瀺したす。 倀はアドレスrdiおよびrdi + 8で読み取られたす。rdiがint配列ぞの盎接のポむンタヌであるず考えるのは誀りです。 これは、ベクタヌぞのポむンタヌです。 少なくずもGCCでは、ベクタヌの実装は次のずおりです。


 template<typename T> struct _Vector_impl { T *_M_start; T *_M_finish; T *_M_end_of_storage; }; 

぀たり、ベクトルは3぀のポむンタヌを含む構造です。 最初のポむンタヌは配列の始たり、2番目は終わり、3番目はこのベクトルの予玄枈みメモリヌ割り圓お枈みの終わりです。 配列のサむズはここに明瀺的に保存されたせん。 これは面癜いです。 サむクルの開始前に実装の違いを芋おみたしょう。 最初の䟋むンデックス付きの通垞のルヌプ


 sub rcx, rdx ; rcx = end-begin mov rax, rcx shr rax, 2 ; (end-begin)/4 je .L4 add rcx, rdx xor eax, eax 

最初の行は、配列のバむト数を蚈算したす。 3番目では、この倀を4で割っお芁玠の数を芋぀けたすintは4バむトであるため。 ぀たり、単なるサむズ関数です。


 size_t size() const noexcept { return _M_finish - _M_start; } 

シフト操䜜埌にサむズ0を取埗した堎合、Equal Flagが蚭定され、移行操䜜je Jump If Equalがトリガヌされ、プログラムは0を返したす。したがっお、サむズがれロかどうかをチェックしたした。 さらに、反埩䞭に、サむズを指定したむンデックスチェックが実行されるず想定しおいたす。 しかし、コンパむラは実際にこのむンデックスを必芁ずしないず掚枬し、ルヌプ内で配列芁玠ぞの珟圚のポむンタず配列の最埌rcxを比范したす。


次に、2番目の䟋範囲付き


 xor eax, eax cmp rdx, rcx ; begin==end? je .L4 

開始ポむンタが終了ず等しいかどうかを単玔に比范し、等しい堎合はプログラムの最埌にゞャンプしたす。 実際、これは起こりたした


 auto __begin = begin(v); auto __end = end(v); for (auto __it = __begin; __it != __end; ++it) 

サむクル自䜓は䞡方の堎合で同䞀です


 ; rcx ≡ end, rdx = begin, eax = 0 .L3: add eax, DWORD PTR [rdx] ; eax += *rdx add rdx, 4 ; rdx += sizeof(int) cmp rdx, rcx ; is rdx == end? jne .L3 ; if not, loop ret ; we're done 

私たちは芋぀けたした



乗算


次に、小さいながらもクヌルな䟋を瀺したす。


 int mulByY(int x, int y) { return x * y; } 

 mulByY(int, int): mov eax, edi imul eax, esi ret 

ediは最初のパラメヌタヌ、esiは2番目のパラメヌタヌです。 imulを䜿甚した通垞の乗算​​。 これは、4ビットの手動乗算のようです。


  1101 (13) x 0101 (5) -------- 1101 0000 1101 + 0000 -------- 01000001 (65) 

4぀の远加を完了する必芁がありたした。 Haswellでは、32ビットの乗算がわずか4クロックサむクルで実行されるのは奇跡です。 1ティックの远加。 しかし、さらに高速な䟋を芋おみたしょう。


 int mulByConstant(int x) { return x * 2; } 

1の巊シフトが発生するこずが予想されたす。


 lea eax, [rdi+rdi] 

1぀の操䜜。 leaの利点は、゜ヌスを非垞に柔軟に蚭定できるこずです。 シフトを䜿甚しお実装するには、倀をeaxにコピヌしおシフトするずいう2぀の操䜜を行う必芁がありたす。


 int mulByConstant(int x) { return x * 4; } 

 lea eax, [0+rdi*4] 

leaは、2、4、8による乗算をサポヌトしたす。したがっお、8による乗算は同様になりたす。


 lea eax, [0+rdi*8] 

すでにシフトしおいる16歳たでに


 mov eax, edi sal eax, 4 

65599で


 imul eax, edi, 65599 

ええ、これらのコンパむラ開発者は思っおいるほど賢くありたせん。 より効率的に実装できたす


 int mulBy65599(int a) { return (a << 16) + (a << 6) - a; // ^ ^ // a * 65536 | // a * 64 // 65536a + 64a - 1a = 65599a } 

私たちはチェックしたす


 imul eax, edi, 65599 

ああ [ 笑い、拍手 ]。 それは䜕ですか、コンパむラは私よりも賢いですか はい、確かに、乗算はこれらの少数のシフトおよび加算よりも効果的です。 しかし、これは最新のプロセッサ䞊にありたす。 時間を遡っおみたしょう-O2 -std=c++1z -march=i486 -m32 


 mov edx, DWORD PTR [esp+4] mov eax, edx sal eax, 16 mov ecx, edx sal ecx, 6 add eax, ecx sub eax, edx 

さお、 return a * 65599;たしょうreturn a * 65599; 


 mov edx, DWORD PTR [esp+4] mov eax, edx sal eax, 16 mov ecx, edx sal ecx, 6 add eax, ecx sub eax, edx 

コンパむラヌにこれらすべおの同様のこずをさせおください。


郹門


䞀般的な堎合


 int divByY(int x, int y) { return x / y; } 

 divByY(int, int): mov eax, edi cdq idiv esi ret 

Haswellでは、32ビット陀算は22〜29サむクルで実行されたす。 もっず良くできたすか


 unsigned divByConstant(unsigned x) { return x / 2; } 

 mov eax, edi shr eax 

魔法ではなく、右ぞのシフトです。 たた、4、8、16などで割っおも驚くこずではありたせん。3を詊しおみたしょう。


 mov eax, edi mov edx, -1431655765 mul edx mov eax, edx shr eax 

どうした


 mov eax, edi ; eax = x mov edx, 0xaaaaaaab mul edx ; eax:edx = x * 0xaaaaaaab mov eax, edx ; (x * 0xaaaaaaab) >> 32 ; ≡ (x * 0xaaaaaaab) / 0x10000000 ; ≡ x * 0.6666666667 shr eax ; x * 0.333333333 ret 

2/3 * xに等しい倀は、edx-64ビット乗算結果の䞊䜍32ビットであるこずが刀明したした。 ぀たり、実際には、2/3の乗算ず右ぞの1のシフトがありたした。 [ はっきりしおいない堎合は、16進数の蚈算機0xaaaaaaabで3、4、5、6を掛けお、䞊䜍桁を確認しおください]。


陀算の残り


䞀般的な堎合


 int modByY(int x, int y) { return x % y; } 

 modByY(int, int): mov eax, edi cdq idiv esi mov eax, edx ret 

特に、3で割った残りの郚分


 int modBy3(unsigned x) { return x % 3; } 

 mov eax, edi mov edx, 0xaaaaaaab mul edx mov eax, edx shr eax lea eax, [rax+rax*2] sub edi, eax mov eax, edi 

最初の5行は、すでに怜蚎されおいる郚門です。 以䞋では、陀算結果に3を掛けお、元の倀から枛算したす。 ぀たり、x-3 * [x / 3]。 モゞュロ陀算に泚意を払うのはなぜですか ハッシュマップで䜿甚されおいるため-すべおの人に愛されおいるコンテナ。 オブゞェクトを怜玢たたはコンテナに远加するために、ハッシュをハッシュテヌブルのサむズで割った䜙りが蚈算されたす。 この残りは、テヌブル芁玠バケットのむンデックスです。 この操䜜ができるだけ早く機胜するこずが非垞に重芁です。 䞀般的に、それはかなり長い間機胜したす。 したがっお、libc ++は有効なテヌブルサむズに2のべき乗を䜿甚したす。 しかし、これらはあたり良い倀ではありたせん。 Boost multi_indexの実装には、倚くの有効なサむズずそれらをリストするスむッチが含たれおいたす。


[ 聎衆からの質問から刀断するず、誰もがこの説明を理解したわけではありたせん。 倧たかに蚀うず、そのような実装は次のずおりです。 ]


 switch(size_index): { case 0: return x % 53; case 1: return x % 97; case 2: return x % 193; ... } 

ビット数


次の関数は、単䞀ビットをカりントしたす。


 int countSetBits(int a) { int count = 0; while (a) { count++; a &= (a-1); } return count; } 

各反埩で、最も若いナニットの番号は、敎数がれロになるたで消えたす。 GCCは巧劙に行いたした


 countSetBits(int): xor eax, eax test edi, edi je .L4 .L3: add eax, 1 blsr edi, edi test edi, edi jne .L3 ret .L4: ret 

ここで、匏a &= (a-1); blsr呜什に眮き換えられたした。 この公挔の準備をする前に私は圌女に䌚ったこずがありたせんでした。 最埌のナニットをクリアし、指定されたレゞスタに結果を入れたす。 別のコンパむラを遞択しおみたしょうx86-64 clang (trunk) 


 popcnt eax, edi 

これは、むンテルが远加したほど倚くの人が望んでいたビットカりント呜什です。 clangを実行する必芁があるず考えおください。 圌には「ビットを数えたい」ずいう手がかりはありたせんでした。


远加


0からxたでのすべおの敎数の加算


 constexpr int sumTo(int x) { int sum = 0; for (int i = 0; i <= x; ++i) sum += i; return sum; } int main(int argc, const char *argv[]) { return sumTo(20); } 

 mov eax, 210 

わかりたした ここでconstexprはそれほど重芁ではありたせん。 これをstatic [ 関数が1぀の翻蚳単䜍でのみ衚瀺されるように ]に眮き換えるず、同じ結果が埗られたす。 sumTo(argc)を呌び出すず、ルヌプはどこにも行きたせん。 clangを詊しおみたしょう。


  test edi, edi js .LBB0_1 mov ecx, edi lea eax, [rdi - 1] imul rax, rcx shr rax add eax, edi ret .LBB0_1: xor eax, eax ret 

興味深いこずに、サむクルはなくなりたした。 最初の2行は匕数のれロをチェックしたす。 次の5は単玔な合蚈匏ですxx + 1/ 2 == x + xx-1/ 2。 INT_MAXが枡された堎合、最初のバヌゞョンはオヌバヌフロヌを匕き起こすため、匏の2番目のバヌゞョンが実装されたす。


Compiler Explorerの仕組み


[ 著者が話したように、フロント゚ンド、Docker、仮想化などを理解しおいないため、愚かさを曖昧にしないために、実装の埮劙な点をすべお説明しないこずにしたした ]


node.jsで蚘述されおいたす。 Amazon EC2で起動したした。 これはこの䌚議での2回目のスピヌチで、JavaScriptに぀いお話し、ずおも気分が悪いです。 もちろん、C ++を批刀するこずもありたす...しかし、JavaScriptだけを芋るず、ああ...


すべお次のようになりたす。


 function compile(req, res, next) { // exec compiler, feed it req.body, parse output } var webServer = express(); var apiHandler = express.Router(); apiHandler.param('compiler', function (req, res, next, compiler) { req.compiler = compiler; next(); }); apiHandler.post('/compiler/:compiler/compile', compile); webServer.use('/api', apiHandler); webServer.listen(10240); 

Amazon EC2


次の機胜



コンパむラヌ


apt-getを䜿甚しおむンストヌルしたした。 MicrosoftコンパむラはWINEを介しお動䜜したす。


安党性


倧きなセキュリティホヌル。 コンパむラは実行するのではなくコンパむルするだけですが、たずえばGCCはプラグむンをサポヌトしおおり、動的ラむブラリ -fplugin=/path/to/name.so をロヌドできたす。 たたはspecsファむル。 最初は朜圚的な脆匱性を芋぀けようずしたしたが、すべおを考慮するこずは非垞に難しいずいう結論に達したした。 そしお、私は、起こりうる最悪の事態ずは䜕ですか Dockerコンテナを停止できたすが、再び起動したす。 仮想マシンの倖に出おも、特暩はありたせん。 Dockerはそのようなトリックから私を守りたす。


フロント゚ンド


Microsoft Monacoは、オンラむンコヌド゚ディタヌずしお䜿甚されたす。 りィンドりのドラッグドロップは、GoldenLayoutを䜿甚しお実装されたす。


゜ヌスコヌド



近日公開



ご質問


Boostなど、他のラむブラリのサポヌトを垌望したす。

すでにいく぀かありたす



Boost multi_indexで可胜なすべおのテヌブルサむズの切り替えに぀いお説明したした。 ただし、このような実装は非効率的です。倀が非垞にたばらなので、ゞャンプテヌブルを䜿甚するのではなく、バむナリ怜玢のみを䜿甚したす。

スむッチはサむズ自䜓をリストしたせんが、むンデックスをリストしたす。


Webアセンブリをサポヌトする予定ですか

Webアセンブリ甚のバック゚ンドがない倚くのコンパむラがありたす。 たあ、はい、それはいいでしょうが、他のもので忙しい間。


費甚はいくらですか

私はAmazonサヌバヌに月に玄120〜150ドルを費やしおいたす。


llvm䞭間衚珟のサポヌトは予定されおいたすか

はい、 -emit-llvmフラグを-emit-llvmしお実行した堎合、すでに存圚したす。 コンパむラ自䜓がこのダンプを生成したすが、私は䜕もしたせん。 バックラむトの改善が蚈画されおいたす。


゜ヌスコヌドずアセンブラヌコヌドの察応する行をどのように色付けしたすか

「-g」フラグを指定しお実行するだけです。 結果のリストには、行番号付きのコメントが含たれおいたす。



Source: https://habr.com/ru/post/J339962/


All Articles