プログラミング
現代C++ CPUのための効率的なC++プログラミング、第4章/パート2
Efficient C++ Programming for Modern C++ CPUs, Chapter 4/part 2 (6it.dev)
要約
本記事は、近刊書籍からの草稿として、現代の64ビットCPUにおける様々なC++操作のCPUサイクルコストを詳細に解説しています。特に、整数乗算と除算が他の整数演算よりも著しく高価である点を強調し、RTTI、C++例外(エラーが稀なケースでは「ゼロコスト例外」が有利)、アトミック操作、そしてインライン化を含む関数呼び出しのパフォーマンスへの影響について議論しています。著者は、マイクロベンチマークの代わりに、操作コストの包括的な図を提供することを目指していますが、数値はおおよそのオーダーであることを注意しています。
全文翻訳
6IT.DevHare on Soft.ware6IT HareOn Soft.wareカテゴリカテゴリ書籍: MOGsのD&D目次Vol. I-IIIの第1ベータ版Vol. IV-VIの第1ベータ版Vol. VII-IXの第1ベータ版On.システムアーキテクチャ要件分析設計上の決定分散システム(Re)アクターOn.プログラミングヒントとコツネットワークプログラミングプログラミング言語最適化デバッグ組み込みOn.ハードウェアMCUWi-FiOn.セキュリティベストプラクティス不正防止研究On.開発開発哲学開発プロセスチーム構成ITキャリアとIT採用その他お知らせレポートIT Hareスクール恐竜のためのIT Hareスクール初心者向けバニーの騒動バニーの裏側で働く人々執筆募集H.A.R.E.とバニー loreインフォグラフィックス: CPUクロックサイクルにおける操作コスト、その22026年6月19日 • "No Bugs" Bunny • Sherry Ignatchenko翻訳"No Bugs" Bunny皮肉屋の建築家 趣味: 大声で考えること、マネージャーと議論すること、人事部を困らせること、率直に物を言うこと、皮肉を言うこと\n\nこれは、Sherry IgnatchenkoとDmytro Ivanchykhinによる近刊の書籍「現代の64ビットCPUのための効率的なC++プログラミング」の第1巻、第4章「CPU物理学とCPUサイクルについて」の第2部草稿です。ご意見をお寄せください。特に、事実と矛盾する点がありましたら、喜んで修正いたします。\n\n第4章の第1部については、https://6it.dev/blog/on-cpu-physics-and-cpu-cycles-80730 を参照してください。\n\n生データ\n\nさて、上記のすべての情報(および[Ignatchenko16a]、[Fog]、および以下にリストされている他の情報源からのビットを追加して)を武器に、多くの実用的な目的でマイクロベンチマークに代わる、そして開発者の語彙における最も一般的な操作のコストを示す、非常に重要な図を作成できます。免責事項: 以下のすべての数値は、おおよそのオーダーでしか正確ではありません!\n\nここでいくつかの項目は上記で議論されておらず、追加の説明が必要な場合があることに注意してください。\n\n乗算/除算\n\nすべてのR-R操作の中で、乗算、特に除算は、パフォーマンスの点でかなり特殊です。乗算と除算以外のすべての整数操作は非常に安価です™ – 通常、レイテンシは1 CPUサイクル、もし特定の操作がCPU設計者によってそれほど重要でないと判断された場合は2 CPUサイクル程度です。さらに、乗算/除算以外の操作では、オペランドのビット数に関係なく、まったく差がないのが一般的です。つまり、乗算と除算以外の操作は、8ビット、16ビット、32ビット、64ビットのオペランドに対して同じ時間がかかる傾向があります。\n\n対照的に、乗算は3-5 CPUサイクルかかる傾向があり、さらに、32ビットと64ビットのオペランドでは異なるCPUサイクル数かかる場合があります。除算ではさらに悪く、現代のCPUでさえ64ビット除算には最大15 CPUサイクルかかることがあります(そして、[Fog]が100+ CPUサイクルとリストしていることを考えれば、これらの数値でさえ感謝すべきです、痛い!)。より新しいCPUの場合、[uops]と[Cortex-A78]は以下の操作レイテンシ(CPUサイクル)を提供しています。\n\nSkylake-X (2017)Zen 2 (2019)Cortex-A78 (2020)Alder Lake-P (2021)Alder Lake-E (2021)Zen 4 (2022)アーキテクチャx64/Intelx64/AMDAArch64x64/Intelx64/Intelx64/AMD\nIMUL R32, R32332 (MUL, W-form)333\nIMUL R64, R64332 (MUL, X-form)353\nIDIV R3223-288-255-12 (DIV, W-form)10-1511-289-14\nIDIV R6437-968-415-20 (DIV, X-form)14-1811-249-19\n\n要約すると(本当に現代の64ビットCPU、つまり約2019-2021年から):あらゆる改善にもかかわらず、除算は依然として非常に高価です™(かつてほどひどく高価ではありませんが)\n約2020年以降、32ビットと64ビットの除算の差はそれほど劇的ではありませんが、依然として存在します。\n乗算については、32ビット版と64ビット版でほぼ同じです(一部のEコアを除く)。TODO: 浮動小数点乗算/除算を追加\n\nRTTI\n\nRTTIはかなり高価になる可能性があり、`dynamic_cast<>`は単純な仮想関数呼び出しの最大5倍も高価になることがあります(原文ママ)[TR][Didriksen]。TODO: 仮想呼び出しと`typeid`と`dynamic_cast`のパフォーマンスをベンチマークする(情報源は非常に矛盾しており、古いデータを提供している可能性があるため、自分たちで実験する必要がある)\n\nただし、特定の主張(例えば[Fog04, section 7.23]を参照)とは異なり、RTTIはクラスオブジェクトのインスタンスに何も追加しないことに注意してください(ポリモーフィックなクラスの場合、RTTIは既にある`vfptr`を使用し、非ポリモーフィックなクラスにはRTTIはまったくありません)。それでも、RTTIはコードサイズを増加させます。\n\nC++例外 vs 戻り値とチェック\n\nC++は長らく、その例外がCスタイルのエラーを戻り値でチェックするよりも効率的であるように設計されているという立場をとってきました。そして実際、エラーが極めて稀なコードでは、それが成り立っているようです。例えば、[Nayar]のマイクロベンチマークは、エラーコードを伴う戻り値がエラーコードなしの戻り値よりも2 CPUサイクル多くかかることを示しています。一方、C++例外が発生した場合、それは非常に高価です™ – [Ongaro]によれば、例外は約5,000サイクル(原文ママ)、[Nayar]によれば約2,700サイクルかかります。これは、100回の関数呼び出しにつき1回の例外がある場合、戻り値を使用する方が良いことを意味しますが、例外1回につき10,000回以上の成功した呼び出しがある場合はC++例外が勝ります。はい、これは[Fog04]の「すべての関数は、たとえ例外が一度も発生しなくても、例外ハンドラのために何らかの情報を保存しなければならない」という主張に反することを知っています – しかし、この記述は2026年現在では絶望的に古いです。[Fog04]のこの観察は、おそらく特別に調整されたスタックフレーム内にアンワインド情報を保存すること([TR, section 5.4]の「The Code Approach」を参照)を指していますが、長い間、すべての主要なコンパイラは[TR]の同じセクションの「The Table Approach」を実装しており、これは現在一般的に「ゼロコスト例外」と呼ばれています。\n\nアトミック、CAS、LL/SC\n\nFSB\n\nフロントサイドバス\n\n歴史的に、アトミック操作(特にCAS=compare-and-swap)は、FSBベースのアーキテクチャでシステム全体の「LOCK」をFSB経由で使用して導入されました。しかし、2000年代初頭にFSBがなくなって以来(x86/x64の世界でNUMAへの切り替えを先駆けたAMD64とOpteronに感謝)、すべてはキャッシュコヒーレンスプロトコル、通常はMESIの何らかのバリエーションに関することになっています。[AlBahra]は、CAS操作に約15 CPUサイクルかかると推定しています。しかし、マルチソケットNUMAシナリオでは、簡単に300-600サイクルに達することがあります。\n\nさらに、アトミック操作には、直接的なレイテンシの増加を超えた、いくつかの不快な副作用があります。FSB時代には、アトミック操作はグローバルバスロックを引き起こし、マルチコア/マルチソケットシステムで顕著なパフォーマンス低下をもたらしました。今日では、FSBがNUMAとMESIライクなプロトコルに置き換えられたことで状況は改善されましたが、それでも「逐次操作間に依存関係がない場合でも、命令レベルの並列処理を妨げ、帯域幅を大幅に制限する(単純な書き込みと比較して最大30倍)」といったかなり重大な影響を引き起こしています[SchweizerEtAl]。書き込みストールなどの関連する影響に関する議論については[Josuttis]も参照してください。\n\nLL/SC\n\nロードリンク/ストアコンディショナル。ストアコンディショナルは、先行するロードリンク以降にその場所に更新がなかった場合にのみデータを保存します。また、RISC-VにおけるアトミックサポートはCAS指向ではなく、LL/SC指向であり、それ自身の利点と癖があることにも注意しましょう。最近のARMはLL/SCとCASの両方をサポートしており、x64は伝統的にCAS指向です。\n\n関数呼び出しとインライン化\n\n歴史的に、[BulkaMayhew, p.115]は、パラメータの数に応じて、関数呼び出しのCPUコストを約25-250サイクルと推定していました。しかし、2000年のこの本は今ではかなり古く感じられ、合理的に信頼できる現代の参考文献は見つかっていません😕。とはいえ、私たち自身の経験に基づくと、控えめな数のパラメータを持つ関数は、直接的なコストとしては15-30サイクル程度かかる傾向があります。この観察は、非Intel CPUについても一貫しているようです。[Ruskin]が指摘しています。\n\n間接呼び出し(関数ポインタを介した呼び出し)については、[Ignatchenko16a]は20-50サイクルと見積もっており、C++仮想関数コストについては30-60サイクルと見積もっており、これも[Ruskin]と一致しているようです。最後に、コンパイラが何をするときに何が起こるかを考慮する必要があります。