【C++学習|豆知識】意外と知らない?std::shared_ptrのメモリ消費と参照カウンタの仕組み

導入

C++開発においてメモリ管理を自動化してくれる std::shared_ptr は非常に強力なツールです。しかし、便利さの裏側で「どれくらいのメモリを消費しているのか」を意識したことはありますか?特にリソースが限られた環境や、膨大な数のオブジェクトを管理する場合、shared_ptr の内部構造を知ることはメモリ最適化の第一歩となります。今回は、あまり語られることのない「参照カウンタ」の仕組みとメモリサイズについて解説します。

基礎知識

std::shared_ptr は、複数のポインタが同じオブジェクトを共有するために「参照カウンタ(Reference Counter)」という仕組みを利用しています。このカウンタは、現在何個の shared_ptr がそのオブジェクトを指しているかを追跡するものです。

このカウンタは、通常「制御ブロック(Control Block)」と呼ばれる動的に確保されるメモリ領域に格納されています。この中には、参照カウントだけでなく、弱参照カウント(std::weak_ptr用)や、デリータ(メモリ解放用の関数)などが含まれることが一般的です。

実装/解決策

参照カウンタのサイズは、多くのプラットフォームで「ワードサイズ(32bitなら4バイト、64bitなら8バイト)」の整数型として実装されており、スレッドセーフを保証するために「原子変数(std::atomic)」として扱われます。

注意すべき点は、std::shared_ptr 自体のサイズは「生ポインタ+制御ブロックへのポインタ」の計2ワード分であることです。つまり、64bit環境であれば、1つの shared_ptr インスタンスにつき16バイトの領域を消費します。さらに、制御ブロックがヒープ上に別途確保されるため、小さなオブジェクトを大量に管理すると、オーバーヘッドが無視できなくなる可能性があります。

サンプルプログラム

以下のコードで、shared_ptr がどれくらいのメモリを消費しているかを確認してみましょう。

include <iostream>
include <memory>

struct Data {
    int value;
};

int main() {
    // shared_ptr自身のサイズを確認
    std::shared_ptr<Data> ptr = std::make_shared<Data>(100);

    // ptr自体のサイズは通常、ポインタ2つ分(64bit環境なら16バイト)
    std::cout < "shared_ptrのサイズ: " << sizeof(ptr) << " バイト" << std::endl;

    // 参照カウンタは制御ブロック内に存在します
    // std::make_shared を使うと、オブジェクトと制御ブロックが
    // ひとつのメモリ領域に確保されるため、メモリ断片化を防げます
    
    return 0;
}

応用・注意点

現場で活用する際の重要なヒントとして、std::make_shared の利用を強く推奨します。

通常、`std::shared_ptr(new T())` と記述すると、「Tの確保」と「制御ブロックの確保」で2回のメモリ確保が発生します。しかし、`std::make_shared()` を使用すれば、これらを一度のメモリ確保で済ませることができます。これにより、ヒープの断片化を抑え、カウンタの管理コストも最適化されます。

最後に注意点ですが、循環参照には気を付けてください。参照カウンタが0にならないとメモリが解放されないため、親と子が互いに shared_ptr で持ち合うとメモリリークが発生します。その場合は、片方を std::weak_ptr に置き換えることで、カウンタを増やさずに参照する設計を心がけましょう。

コメント

タイトルとURLをコピーしました