web-dev-qa-db-ja.com

デフォルトのnew演算子とdelete演算子を置き換えるのはなぜですか?

理由 すべき デフォルトの演算子newdeleteをカスタムのnewdelete演算子に置き換えますか?

これは、非常に明快なC++ FAQの Overloading new and delete の続きです:
演算子のオーバーロード。

このFAQへのフォローアップエントリは:
ISO C++標準準拠のカスタムnewおよびdelete演算子はどのように記述する必要がありますか?

注:答えは、Scott Meyersのより効果的なC++の教訓に基づいています。
(注:これは、 Stack OverflowのC++ FAQ へのエントリとなることを意味します。FAQこのフォームでは、 このすべてを開始したメタへの投稿 がその場所になります。その質問に対する回答は C++チャットルーム で監視されます、ここでFAQアイデアはそもそも始まったので、あなたの答えはアイデアを思いついた人に読まれそうです。)

64
Alok Save

new演算子とdelete演算子を置き換えるには、いくつかの理由があります。

使用エラーを検出するには:

newdeleteの誤った使用により、Undefined Behaviorメモリリーク。それぞれの例を次に示します。
deleteedメモリで複数のnewを使用し、deleteを使用して割り当てられたメモリでnewを呼び出さない。
オーバーロードされた演算子newは割り当てられたアドレスのリストを保持でき、オーバーロードされた演算子deleteはリストからアドレスを削除できるため、このような使用エラーを簡単に検出できます。

同様に、さまざまなプログラミングの間違いにより、データオーバーラン(割り当てられたブロックの終わりを超えて書き込む)およびアンダーラン(割り当てられたブロックの開始前に書き込む)につながる可能性があります。
オーバーロードされた演算子newは、ブロックを過剰に割り当てて、クライアントが使用できるようにするメモリの前後に既知のバイトパターン(「シグネチャ」)を配置できます。オーバーロードされたオペレーターの削除では、署名がまだ残っているかどうかを確認できます。したがって、これらの署名が損なわれていないかどうかを確認することで、割り当てられたブロックの寿命中にオーバーランまたはアンダーランが発生したことを判断することができ、オペレーターの削除はその事実と問題のポインターの値をログに記録することができます。適切な診断情報を提供します。


効率を改善するには(速度とメモリ):

new演算子とdelete演算子は、すべての人にとって適切に機能しますが、誰にも最適ではありません。この動作は、それらが一般的な使用のみを目的として設計されているという事実から発生します。それらは、プログラムの期間中に存在するいくつかのブロックの動的な割り当てから、多数の短命オブジェクトの一定の割り当ておよび割り当て解除に至るまでの割り当てパターンに対応する必要があります。最終的に、コンパイラーに同梱される演算子newおよび演算子deleteは、中道戦略を取ります。

プログラムの動的なメモリ使用パターンを十分に理解している場合、多くの場合、operator newおよびoperator deleteのカスタムバージョンがデフォルトのパフォーマンスよりも優れている(パフォーマンスが速い、または必要なメモリが50%少ない)ことがわかります。もちろん、あなたが何をしているのか確信がない限り、これを行うのは良い考えではありません(複雑さを理解していないなら、これを試してはいけません)。


使用統計を収集するには:

#2で述べたように効率を改善するためにnewdeleteを置き換えることを考える前に、アプリケーション/プログラムが動的割り当てを使用する方法に関する情報を収集する必要があります。以下に関する情報を収集することができます。
割り当てブロックの分配、
寿命の分布、
割り当ての順序(FIFOまたはLIFOまたはランダム)、
一定期間にわたる使用パターンの変化の理解、使用される動的メモリの最大量など。

また、次のような使用情報を収集する必要がある場合があります。
クラスの動的オブジェクトの数をカウントし、
動的割り当てなどを使用して作成されるオブジェクトの数を制限します。

すべて、この情報は、カスタムnewdeleteを置き換え、オーバーロードされたnewdeleteに診断収集メカニズムを追加することで収集できます。


newの準最適なメモリアライメントを補正するには:

多くのコンピュータアーキテクチャでは、特定の種類のデータを特定の種類のアドレスのメモリに配置する必要があります。例えば、アーキテクチャーでは、ポインターが4の倍数のアドレスで発生する(つまり、4バイトでアライメントされる)か、ダブルが8の倍数のアドレスで発生する(つまり、8バイトでアライメントされる)必要があります。このような制約に従わないと、実行時にハードウェア例外が発生する可能性があります。他のアーキテクチャはより寛容であり、パフォーマンスを低下させても動作する可能性があります。一部のコンパイラに付属する演算子newは、doubleの動的割り当ての8バイトのアライメントを保証しません。そのような場合、デフォルトの演算子newを8バイトのアライメントを保証するものに置き換えると、プログラムのパフォーマンスが大幅に向上し、newdeleteを置き換えるのに十分な理由になります。演算子。


関連オブジェクトを互いに近くにクラスター化するには:

特定のデータ構造が通常一緒に使用されていることを知っていて、データを操作するときにページフォールトの頻度を最小限にしたい場合は、データ構造が別のヒープにクラスター化されるように、データ構造用に別のヒープを作成するのが理にかなっています可能な限りページ。 newおよびdeleteのカスタム配置バージョンにより、このようなクラスタリングを実現できます。


型にはまらない動作を取得するには:

コンパイラーが提供するバージョンでは提供されていないことを行うために、オペレーターにnewおよびdeleteを使用したい場合があります。
たとえば:アプリケーションデータのセキュリティを強化するために、割り当て解除されたメモリをゼロで上書きするカスタム演算子deleteを作成できます。

66
Alok Save

まず、本当に多くの異なるnewおよびdelete演算子があります(実際には任意の数)。

まず、::operator new::operator new[]::operator deleteおよび::operator delete[]。第二に、クラスXには、X::operator newX::operator new[]X::operator deleteおよびX::operator delete[]

これらの間では、グローバル演算子よりもクラス固有の演算子をオーバーロードすることがはるかに一般的です-特定のクラスのメモリ使用量が、デフォルトを大幅に改善する演算子を書くことができる特定の十分なパターンに従うことはかなり一般的です。一般的に、メモリ使用量をグローバルベースでほぼ正確または具体的に予測することははるかに困難です。

おそらく言及する価値もありますが、operator newおよびoperator new[]は互いに独立しています(X::operator newおよびX::operator new[])、2つの要件に違いはありません。 1つは単一のオブジェクトを割り当てるために呼び出され、もう1つはオブジェクトの配列を割り当てるために呼び出されますが、それぞれが必要なメモリ量を受け取るだけで、(少なくとも)その大きさのメモリブロックのアドレスを返す必要があります。

要件といえば、おそらく他の要件を確認する価値があります1:グローバル演算子は真にグローバルである必要があります-名前空間または内に1つ入れないでください。特定の翻訳単位で1つを静的にします。つまり、オーバーロードが発生する可能性があるのは、クラス固有のオーバーロードまたはグローバルオーバーロードの2つのレベルのみです。 「名前空間Xのすべてのクラス」や「変換単位Yのすべての割り当て」などの中間点は許可されません。クラス固有の演算子はstaticである必要がありますが、実際に静的として宣言する必要はありません-それらは staticを明示的に宣言するかどうか。公式には、グローバル演算子は多くの場合、整列されたメモリを返すため、あらゆるタイプのオブジェクトに使用できます。非公式には、1つの点で小さな揺れ部屋があります:小さなブロック(2バイトなど)のリクエストを受け取った場合、そのサイズまでのオブジェクトに合わせてメモリを提供するだけで十分です。とにかく未定義の動作につながります。

これらの準備をカバーしたので、これらの演算子をオーバーロードしたいwhyについての元の質問に戻りましょう。まず、グローバル演算子をオーバーロードする理由は、クラス固有の演算子をオーバーロードする理由と大幅に異なる傾向があることを指摘する必要があります。

より一般的であるため、最初にクラス固有の演算子について説明します。クラス固有のメモリ管理の主な理由はパフォーマンスです。これは通常、2つの形式のいずれか(または両方)で提供されます。速度を向上させるか、断片化を減らすかのいずれかです。メモリマネージャが特定のサイズのブロックを処理するonlyにより速度が向上するため、ブロックが十分に大きいかどうかを確認したり、大きすぎる場合にブロックを2つに分割したりするなどの時間を費やします。断片化は(ほぼ)同じ方法で削減されます。たとえば、N個のオブジェクトに十分なN個のオブジェクトに必要なスペース。 1つのオブジェクトに相当するメモリを割り当てると、1バイト以上ではなく、1つのオブジェクトにexactlyスペースを割り当てます。

グローバルメモリ管理オペレータに過負荷をかける理由は非常に多くあります。これらの多くは、アプリケーションが必要とする合計メモリの追跡(組み込みシステムへの移植の準備など)や、メモリの割り当てと解放の不一致を示すことによるメモリの問題のデバッグなど、デバッグまたは計装に向けられています。別の一般的な戦略は、要求された各ブロックの境界の前後に追加のメモリを割り当て、それらの領域に一意のパターンを書き込むことです。実行の最後に(場合によっては他の時間にも)、それらの領域を調べて、割り当てられた境界の外側にコードが書き込まれているかどうかを確認します。さらに別の方法は、 自動化されたガベージコレクター などを使用して、メモリの割り当てまたは削除の少なくともいくつかの側面を自動化することにより、使いやすさを向上させることです。

デフォルト以外のグローバルアロケーターを使用して、パフォーマンスを向上させることもできます。典型的なケースは、一般的には低速なデフォルトのアロケーターを置き換えることです(例えば、少なくとも4.xのMS VC++のいくつかのバージョンは、システムのHeapAllocおよびHeapFree関数をevery割り当て/削除操作)。実際に見た別の可能性は、IntelプロセッサでSSE操作を使用したときに発生しました。これらは128ビットデータで動作します。操作はアライメントに関係なく動作しますが、データは128ビット境界に揃えられます。一部のコンパイラ(MS VC++など)2)必ずしもその大きな境界にアライメントを強制しているわけではないため、デフォルトのアロケーターを使用するコードは機能しますが、割り当てを置き換えると、これらの操作の速度が大幅に向上します。


  1. 要件の大部分は、C++標準の§3.7.3および§18.4(またはC++ 0xの§3.7.4および§18.6、少なくともN3291以降)でカバーされています。
  2. 私はMicrosoftのコンパイラを選ぶつもりはないことを指摘する義務があると感じています-私はそれがそのような問題の異常な数を持っているとは思いませんが、私はたまたまそれを頻繁に使用するので、私はその問題をかなり意識する傾向があります。
12
Jerry Coffin

多くのコンピュータアーキテクチャでは、特定の種類のデータを特定の種類のアドレスのメモリに配置する必要があります。例えば、アーキテクチャーでは、ポインターが4の倍数のアドレスで発生する(つまり、4バイトでアライメントされる)か、ダブルが8の倍数のアドレスで発生する(つまり、8バイトでアライメントされる)必要があります。このような制約に従わないと、実行時にハードウェア例外が発生する可能性があります。他のアーキテクチャはより寛容であり、パフォーマンスは低下しますが機能する場合があります。

明確にするために:アーキテクチャrequiresたとえば、doubleデータが8バイトにアライメントされている場合、最適化するものはありません。適切なサイズのあらゆる種類の動的割り当て(例:malloc(size)operator new(size)operator new[](size)、_new char[size]_ここで、size >= sizeof(double))は適切に配置されることが保証されます。実装がこの保証を行わない場合、準拠していません。その場合に_operator new_を変更して「正しいこと」を行うことは、最適化ではなく、実装を「修正」する試みです。

一方、一部のアーキテクチャでは、1つ以上のデータ型に対して異なる(またはすべての)種類のアライメントを許可しますが、同じタイプのアライメントに応じて異なるパフォーマンス保証を提供します。実装は、メモリを返す可能性があります(再び、適切なサイズの要求を想定して)準最適に整列され、まだ適合しています。これが例の目的です。

6
Luc Danton

使用統計に関連:サブシステムごとの予算。たとえば、コンソールベースのゲームでは、3Dモデルジオメトリ用、テクスチャ用、サウンド用、ゲームスクリプト用など、メモリの一部を予約したい場合があります。カスタムアロケータは、サブシステムごとに各割り当てにタグ付けして、個々の予算を超過した場合の警告。

4

一部のコンパイラに同梱されているnew演算子は、doubleの動的割り当ての8バイトのアライメントを保証しません。

引用してください。通常、デフォルトのnew演算子はmallocラッパーよりも少しだけ複雑です。mallocラッパーは、標準では[〜#〜] any [〜#〜]ターゲットアーキテクチャがサポートします。

新しいクラスをオーバーロードして独自のクラスを削除する正当な理由がないと言っているわけではありません...ここでいくつかの正当なクラスに触れましたが、上記はそれらの1つではありません。

4
Mark

「グローバルな新規作成と削除をオーバーロードする理由」からの私の回答 ここからリストを繰り返す価値があるようです-その回答を参照してください(または実際 その質問に対する他の回答 )より詳細な議論、参照、およびその他の理由のため。これらの理由は通常、ローカル演算子のオーバーロードとデフォルト/グローバルのオーバーロード、およびC malloc/calloc/realloc/freeオーバーロードまたはフックにも当てはまります。 。

グローバルなnewをオーバーロードし、多くの理由で作業している演算子を削除します。

  • プーリングすべての小さな割り当て-オーバーヘッドを減らし、断片化を減らし、小さな割り当てが多いアプリのパフォーマンスを向上させることができます
  • フレーミング既知のライフタイムの割り当て-この期間の最後まですべての解放を無視し、それらをすべて解放します(確かに、グローバルよりもローカルオペレーターのオーバーロードでこれを行います)
  • alignment調整-キャッシュライン境界などへ
  • alloc fill-初期化されていない変数の使用法の公開を支援
  • free fill-以前に削除されたメモリの使用量の公開を支援
  • delayed free-フリーフィルの有効性を高め、ときどきパフォーマンスを向上させます
  • sentinelsまたはfenceposts-バッファーオーバーラン、アンダーラン、および時々発生するワイルドポインターの公開を支援
  • リダイレクト割り当て-NUMA、特別なメモリ領域を考慮するため、またはメモリ内の個別のシステムを分離するため(埋め込みスクリプト言語やDSLなど)
  • ガベージコレクションまたはクリーンアップ-埋め込みスクリプト言語にも役立ちます
  • ヒープ検証-N個のallocs/freesごとにヒープデータ構造を調べて、すべてが正常に見えることを確認できます
  • accountingleak trackingおよびusage snapshots/statistics(stacks、allocation年齢など)
3
leander

特定の共有メモリ領域にオブジェクトを割り当てるために使用しました。 (これは@Russell Borogoveが言ったことに似ています。)

何年も前に [〜#〜] cave [〜#〜] のソフトウェアを開発しました。これは、マルチウォールVRシステムです。 1台のコンピューターを使用して各プロジェクターを駆動しました。 6が最大(4つの壁、床、天井)で、3がより一般的でした(2つの壁と床)。マシンは、特別な共有メモリハードウェアを介して通信しました。

これをサポートするために、通常の(非CAVE)シーンクラスから派生して、シーン情報を共有メモリアリーナに直接配置する新しい「新しい」ものを使用しました。次に、そのポインターを異なるマシンのスレーブレンダラーに渡しました。

3
Andrew Dalke