メモリリークとの戦い(C ++ CRT)

メモリリークは非常に深刻で危険な問題です。 ユーザーは32Kbのメモリの単一のリークに気付かない可能性があります(これは640Kbの5%全体であり、これは「すべての人に十分」です )が、 INT_MAX64ビットアーキテクチャ )私たちはそれを苦しみ、私たちの製品は失敗する運命にあります。

状況を防ぐことは難しいことではないようです-「すべてを所定の場所に置く」ルールを使用しますが、実際には、たとえば例外を使用するため、ヒューマンファクター(banal不注意)、アーキテクチャのarchitectureさ、およびオペレーター実行の非線形順序によって非常に複雑になります

また、パフォーマンスを犠牲にして自動ガベージコレクターに「降伏」することができます(これは必ずしもマネージC ++ではありません。ネイティブC ++ / Cにはガベージコレクションライブラリなどがあります)。

ただし、「すべてが悪い」状況については説明します。

その場合、タスクは潜在的なリークを検出して修正することになります-修正に関してはここではすべてが簡単です( deleteまたはdelete[] )。 しかし、リークを検出する方法は? グーグルは答えを喜んで教えてくれるでしょう。

ただし、デバッグCRTを使用すると、より簡単に実行できます。

ステップ1.リーケージアカウンティングを有効にする


これを行うには、Debug CRTヘッダーを接続し、Debug Heap Alloc Mapの使用を有効にします。
#ifdef _DEBUG
#include <crtdbg.h>
#define _CRTDBG_MAP_ALLOC
#endif


* This source code was highlighted with Source Code Highlighter .

さて、 newまたはmalloc()を介してメモリを割り当てるmalloc()データは次の構造にラップされます( しかし、実際には少しずるいです、データを担当するフィールドはstructの構文に対応せず、「構造体」自体はCRT内のどこかで定義され、その説明はプログラマ向けではありません利用可能 ):

typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader * pBlockHeaderNext;
struct _CrtMemBlockHeader * pBlockHeaderPrev;
char * szFileName;
int nLine;
size_t nDataSize;
int nBlackUse;
long lRequest;
unsigned char gap[nNoMansLandSize];
unsigned char data[nDataSize];
unsigned char anotherGap[nNoMansLandSize];
} _CrtMemBlockHeader;


* This source code was highlighted with Source Code Highlighter .

これには、 szFileNameファイルszFileNameと、メモリが割り当てられたnDataSize 、要求されたnDataSizeメモリの量、および実際には、いわゆるNo Mans Landエリアにラップされたデータdataに関する情報が含まれます。 BlockHeader自体BlockHeader二重にリンクされたリストに編成されているため、リストを簡単に作成でき、それに応じて、対応するリリース操作がなかったすべてのメモリ割り当て操作を識別できます。

ステップ2.リークのリスト


CrtMemBlockHeaderリストをCrtMemBlockHeaderて、問題のある領域に関する情報を提供する関数が必要です。

_CrtDumpMemoryLeaks();

次に、[デバッグ出力]ウィンドウに次の情報が表示されます。
Detected memory leaks!
Dumping objects ->
{163} normal block at 0x00128788, 4 bytes long.
Data: < > 00 00 00 00
{162} normal block at 0x00128748, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.


* This source code was highlighted with Source Code Highlighter .

そして、それはほとんどクールですが、この結果はいくつかの理由でまだ使用できません:

...また、単にグローバルオブジェクトから「戻る」時間がなかったもの。 そして、グローバルオブジェクトはおそらく悪いかもしれませんが、今ではそれらがそうであるという考えに慣れましょう。つまり、何らかの方法でそれらを_CrtDumpMemoryLeaks()出力から削除する必要があるということです。 そして、これは次のトリックによって解決されます。

int _tmain( int argc, _TCHAR* argv[])
{
_CrtMemState _ms;
_CrtMemCheckpoint(&_ms);

// some logic goes here...

_CrtMemDumpAllObjectsSince(&_ms);
return 0;
}

* This source code was highlighted with Source Code Highlighter .

初期(メインに入る時点の)メモリ状態( _CrtMemCheckpoint )を特別な構造に書き込み、アプリケーションを終了する前に、 _ms後に作成されたメモリ内の残りのオブジェクト( _CrtMemDumpAllObjectsSince )を表示します。これらは「リーク」です。 これで情報が正しいので、その利便性を考慮します。

ステップ3.結果の表示


出力のリダイレクトは非常に簡単です。ここでは、 _CrtSetReportModeおよび_CrtSetReportFile関数が役立ちます。

_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_FILE );
_CrtSetReportFile( _CRT_WARN, _CRTDBG_FILE_STDOUT );


* This source code was highlighted with Source Code Highlighter .

これで、すべての警告の出力( _CrtMemDumpAllObjectsSince出力)がstdoutに直接_CrtMemDumpAllObjectsSinceれます。 _CrtSetReportFile関数の2番目のパラメーターは、実際のファイルハンドルに設定できます。

メモリ割り当てが発生したファイル名と行が表示されないのはなぜですか? Microsoft Visual C ++ 6.0によると、 crtdbg.hヘッダーのnew関数の次の再定義がこの情報に関与していることがcrtdbg.hました。

#ifdef _CRTDBG_MAP_ALLOC
inline void * __cdecl operator new (unsigned int s)
{ return :: operator new (s, _NORMAL_BLOCK, __FILE__, __LINE__); }
#endif /* _CRTDBG_MAP_ALLOC */

* This source code was highlighted with Source Code Highlighter .

そして、推測するのは難しくありません。目的の結果が得られませんでした__FILE__:__LINE__ 常に「crtdbg.h file line 512」にデプロイされていました 。 そして、Microsoftのスタッフがこの「機能」を完全に削除し、プログラマに提供しました。 1つの定義でこの機能を実現できるため、それは怖いことではありません。
#define new new ( _NORMAL_BLOCK, __FILE__, __LINE__)

* This source code was highlighted with Source Code Highlighter .

一般的なヘッダーファイルに含めることを強くお勧めします( crtdbg.h後に含める必要があります)。 newが既にオーバーライドされている場合、問題が発生します。 私が見ているように、newのいくつかの合理的な再定義はCRTを使用しません(そうでなければフック技術を使用することは可能です)、そしてこの場合、スキームはまったく適用されません、大丈夫です。

一般的に、今では彼らは望んでいたものを手に入れました。 ここに結論の例がありますが、どうあるべきかはすでに明らかです。

計算時間


もちろん、CRT Internals構造の編成とサポートには時間がかかり、追加のメモリが必要です。 いくらですか?

UPD:以下はすべてWin32でのみ有効です(Vista SP1でテスト済み)。

new (理論的には40Mbのメモリ)を使用して1000万のintを作成します。
CRTのデバッグ〜500Mb3秒
リリース〜160Mb1秒

リリースビルドの〜160Mbの数値は、少し驚かれるかもしれません。 しかし、これは正常ですHeapAllocは、複数の16アドレスにデータを整列させるHeapAlloc OS関数を介してメモリを割り当てます(Win32の場合)。 1文字のメモリを割り当てると、さらに15バイトが得られます。これを使用して、何か悪いこともできます(確かに行う必要はありません)。 デバッグの場合、非常に予測可能な結果-別のsizeof(_CrtMemBlockHeader)に1000万を掛けて追加すると、正確に500メガバイトになります。

興味深い経験的な結果により、このリリースでは、 new intHeapAllocよりも4バイト遅くなり、速度はnew int() (デフォルトで初期化、つまりゼロ)とほとんど区別できず、5 HeapAlloc速くなりますフラグHEAP_ZERO_MEMORY HeapAllocしたHeapAllocよりも-10%

さて、今ではnew int[256] (理論的には128Mbのメモリ)を経由して、128千int [256]:
CRTのデバッグ〜136Mb172ミリ秒
リリース〜128.5Mb60ミリ秒

結果は予測可能であり、非常に満足のいくものです。 さまざまなデータを混合し、メモリを部分的に解放する場合を含む、サイズが異なるデータでも1:3の速度比が確認されました。 ただし、動的メモリ操作をデバッグしないと、コードはリリースよりも数倍遅くなります!

おわりに


メモリリークは素手で対処できます。 もちろん、「生の」出力は、リークツリーや、リーク全体の降順で並べ替えられたコードプレースのリストほど効果的ではありません(ただし、これはすべて、私たちの結論によると、難なく生成できます)。 しかし、小規模なプロジェクトやタスクでは、このトリックを行うことができます。 また、このメソッドはサポートを必要とせず( newの再定義のために「書かれて忘れられた」わけではありませんが、それに近い )、エントリーレベルは本格的なアナライザーよりもはるかに低くなっています。

まあ、おそらくそれだけです。 それが全体像の再構築のですか?

UPD:メソッドは、外部アナライザーと競合しません。 目標は多少異なりますが、スタンディングツールの言及は大歓迎です(繰り返しはせず、お願いします)。

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


All Articles