C++における厳密なエイリアシングルールは、異なる型のオブジェクトにアクセスするために一つの型のポインタを逆参照することを禁止しています。これにより、レジスタキャッシングなどの重要なコンパイラ最適化が可能になります。C++17以前は、開発者は生のメモリを調査するためにcharやunsigned charに頼っていましたが、これらの型は安全でない算術を促進し、意図を明確に示しませんでした。C++17では、バイトレベルのメモリアクセス用の専用型としてstd::byteが導入され、算術に参加せずに任意のオブジェクトをエイリアスできるようになりました。また、オブジェクトが破棄されたオブジェクトによって以前占有されていたストレージ内に作成される場合のポインタの由来の問題を解決するためにstd::launderが追加されました。
オブジェクトが破棄され、同じアドレスに新しいオブジェクトが構築されると(メモリプールやベクタの再配置で一般的)、元のポインタは無効になり、ビットパターンはそのままでも、std::byteへのポインタは新しいオブジェクトに関する型情報を持たず、コンパイラはそこに古いオブジェクト(またはオブジェクトが存在しない)と仮定する可能性があります。その結果、書き込みを破棄したり、読み込みを再順序化するような攻撃的な最適化が行われます。std::launderなしで、std::byteバッファから導出されたポインタを通じて新しいオブジェクトにアクセスすると、コンパイラがオブジェクトのライフタイム遷移を追跡できないため、未定義の動作が発生します。
std::launderは、指定されたアドレスに特定の型の新しいオブジェクトが存在することをコンパイラに明示的に通知し、エイリアス解析のための新しいオブジェクトを正しく指すポインタを返します。ストレージ管理にstd::byte*を組み合わせるパターンでは、std::byte[]として生のストレージを割り当て、配置newまたはstd::construct_atを介してオブジェクトを構築し、その後std::launderを使用して有効な型付きポインタを取得します。これにより、コンパイラは新しいオブジェクトのライフタイムと型を尊重し、厳密なエイリアシングルールに違反することなく最適化が安全に進行できるようになります。
#include <new> #include <cstddef> #include <iostream> struct Widget { int value; }; int main() { alignas(Widget) std::byte buffer[sizeof(Widget)]; // オブジェクトを作成 Widget* w1 = new (buffer) Widget{42}; // オブジェクトを破棄 w1->~Widget(); // 同じアドレスに新しいオブジェクトを作成 Widget* w2 = new (buffer) Widget{99}; // std::launderなしでは、これは技術的にUBです // std::byte* ptr = buffer; // Widget* w3 = reinterpret_cast<Widget*>(ptr); // 危険! // 正しい方法 Widget* w3 = std::launder(reinterpret_cast<Widget*>(buffer)); std::cout << w3->value << '\n'; }
低遅延のトレーディングシステムでは、メモリの断片化を避けるためにstd::byteの事前に割り当てられた配列を使用して、金融MarketEvent構造体を保存するRingBufferを実装しました。トレーディングアルゴリズムによってイベントが消費されると、私たちは明示的にそれらを破棄し、その代わりに新しいイベントを構築して、追加の割り当てなしでメモリを再利用しました。プロファイリング中に、コンパイラがイベントのタイムスタンプの読み込みを再順序化しており、新しく書き込まれたイベント状態の代わりに、CPUキャッシュから古いデータを読み込んでいることがわかりました。
プロファイリング中に、コンパイラがイベントのタイムスタンプの読み込みを再順序化しており、新しく書き込まれたイベント状態の代わりに古いデータをキャッシュから読み込んでいることに気付きました。この問題は、オプティマイザがメモリ位置にまだ古い破棄されたイベントが存在すると仮定したときに現れました。配置new操作が新しいタイムスタンプを書き込んだにもかかわらず、厳密なエイリアシングルールにより、コンパイラは古いキャッシュ値をレジスタに保持し、新しい書き込みをバッファに無視していました。
この最適化バリアを解決するために、私たちは3つの異なるアプローチを検討しました。最初のアプローチは、バッファをvolatileとしてマークすることでしたが、これによりメモリアクセスがRAMに強制され、すべてのレジスタ最適化が無効になるため、パフォーマンスが大幅に低下します。また、根本的な厳密なエイリアシング違反にも対処できず、単にハードウェアバリアで症状をマスクするだけであるため、ホットパスでの受容可能なレイテンシのためにこれを却下しました。
二つ目のアプローチは、バッファへのアクセスの周囲にstd::atomic_thread_fenceを取得し、リリースセマンティクスを使用しました。これにより、スレッド間の書き込みの可視性は確保されますが、その作成から派生していないポインタを介してオブジェクトにアクセスするという根本的な未定義動作は解決されません。これは単一スレッドのコンテキストに対して不必要なオーバーヘッドを追加し、正しいエイリアス分析に必要な型情報をコンパイラに提供しませんでした。
三つ目のアプローチでは、std::construct_at(C++20)を使用した構築の後にstd::launderを使用して適切な型付きポインタを取得しました。この組み合わせにより、オプティマイザにオブジェクトのライフタイムと正確な型を明示的に通知し、正しく値をキャッシュさせながら新しいオブジェクトの状態を尊重することができます。このソリューションを選択した理由は、標準に準拠した正しいセマンティクスを提供し、実行時のオーバーヘッドがゼロであることが保証されているからです。
std::launderを実装した後、コンパイラはタイムスタンプの読み込みを再順序化することをやめ、メモリフェンスやボラタイルアクセスを追加することなく競合状態を排除しました。システムはマイクロ秒未満のレイテンシ要件を維持しつつ、**C++**標準に完全に準拠しました。これにより、オブジェクトのライフタイムルールを理解することが高性能なシステムプログラミングにおいて重要であることが検証されました。
std::byteが任意の型をエイリアスできる場合、なぜstd::byteポインタを介してオブジェクトを変更することは、オブジェクトがconstでないことが必要ですか?
std::byteはオブジェクト表現へのアクセスのためのエイリアス免除を提供しますが、それはオブジェクト自体のconst資格を上書きすることはありません。**C++**標準は、任意のポインタ型(std::byte*を含む)を介してconstオブジェクトを変更することは未定義の動作につながると定義しています。厳密なエイリアシングルールとconst適合性ルールは独立して動作します。std::byteが型アクセスの問題を解決する一方で、書き込み許可の問題は解決していません。候補者は生のバイトを表示する能力とconst意味をバイパスする能力を混同することがよくあります。
なぜstd::launderが必要なのか、配置newがすでに作成されたオブジェクトへのポインタを返すのに?
配置newは正しい型のポインタを返しますが、そのポインタがオブジェクトのライフタイムが始まる前に計算されたvoidまたはstd::byteから派生している場合、コンパイラは返されたアドレスがその位置における以前のオブジェクトとは異なる新しいオブジェクトを指していることを認識しない可能性があります。std::launderは、新しいオブジェクトの指定された型を含むアドレスとしてコンパイラに扱うように指示して、新しいポインタの由来を確立する最適化バリアを作成します。ラウンダリングなしでは、コンパイラはバッファへのポインタがまだ古い破棄されたオブジェクトを指していると想定し、不正なデッドストアの排除や値の伝播が発生します。
C++20の暗黙のオブジェクト作成は、std::byteバッファとstd::launder間の相互作用をどのように変えますか?
C++20は暗黙のオブジェクト作成を導入しました。つまり、std::construct_atやmemcpyによるstd::byte配列での操作が、明示的な配置new構文なしでオブジェクトを暗黙的に作成できることを意味します。しかし、std::launderは、元のstd::byteからそれらの暗黙的に作成されたオブジェクトへの使用可能なポインタを取得するために依然として必要です。暗黙の作成は、オブジェクトがライフタイム目的に存在することを確立しますが、std::launderを使用してTの適切な型ポインタにstd::byte*を変換する必要があります。候補者は、暗黙的な作成がstd::launderの必要性を排除すると信じることがよくありますが、これら二つの機能は異なる問題を解決します。一方はライフタイムを管理し、もう一方はポインタの由来を管理します。