May 20

某プロジェクトのための実益も兼ねてBoostの共有メモリ系ライブラリで遊んでみた。offset_ptrがかなり直感的に使えるので、共有メモリを使わないコードをまずそれで書いてみて、のちのち共有メモリ型の実装に発展させるというのは比較的簡単にできそう。あと、managed_shared_memoryとmanaged_mapped_fileクラスを使うと、共有メモリ上のメモリ管理(動的な領域の確保と解放、さらに確保した領域上でのオブジェクトの構築)もほぼおまかせでできるので、少なくともメモリ管理の効率とかを気にしない範囲でなら共有メモリ対応も結構簡単にできそう。

例題として単純な一方向linked listをいじるコードを書いてみた。以下はリストのエントリの定義。メンバとして”next”へのポインタとエントリの値(int)を持つ、よくある定義だけど、nextがEntry*じゃなくてoffset_ptrになっているところが違う。

allocatedeallocateはリストエントリを動的に生成・破棄するためのnewdelete相当の関数。種々の(共有)メモリ管理方式に一括して対応するためにテンプレートにしている。SegmentTypeとしてはたとえばmanaged_shared_memoryを想定している。

struct Entry;
typedef offset_ptr<Entry> EntryPtr;

struct Entry {
    EntryPtr next;
    const int value;

    Entry(int val) : next(NULL), value(val) {}
    template <typename SegmentType>
    static Entry* allocate(SegmentType& segment, int i);
    template <typename SegmentType>
    static void deallocate(SegmentType& segment, EntryPtr ep) {
        segment.destroy_ptr(ep.get());
    }
};

以下の二つはBoostのmanaged_shared_memoryとmanaged_mapped_fileのためのallocateの定義。managed_xxxはともに内部にメモリアロケータ(オブジェクトのコンストラクトもやってくれる)も備えているので単にそれを呼ぶだけ。これらは共有メモリを参照しているプロセスから単一の識別子で参照される必要はない(すでに知っているポインタからたぐっていけるはず)のでanonymous_instanceを指定する。

template <>
Entry*
Entry::allocate<managed_shared_memory>(
    managed_shared_memory& segment, int i) 
{
    return (segment.construct<Entry>(
                anonymous_instance)(i));
}

template <>
Entry*
Entry::allocate<managed_mapped_file>(
    managed_mapped_file& segment, int i) 
{
    return (segment.construct<Entry>(
                anonymous_instance)(i));
}

次が大事な部分: リストに要素を追加、リストから要素を削除、リストをなめて内容を表示する関数。追加と削除ではメモリ管理が必要になるのでテンプレート化した上でSegmentTypeを渡している。重要なのはEntryPtr(offset_ptr)の使い方。このコード自体はoffset_ptr型の特徴には依存していないので、Entry*と(ほぼ)互換性のある形になっている。(get_offset()だけはoffset_ptrクラス依存だけど、これはここでの作業の本質ではなくてデバッグ用出力なので無視してよい)

template <typename SegmentType>
void
addListEntry(SegmentType& segment,
             EntryPtr* list_head,
             int i)
{
    EntryPtr* prev = list_head;
    while (*prev) {
        prev = &(*prev)->next;
    }
    *prev = Entry::allocate(segment, i);
}

template <typename SegmentType>
void
removeListEntry(SegmentType& segment,
                EntryPtr* list_head, int i)
{
    EntryPtr* prev = list_head;
    for (EntryPtr ep = *list_head;
         ep;
         ep = ep->next)
    {
        if (ep->value == i) {
            *prev = ep->next;
            Entry::deallocate(segment, ep);
            return;
        }
        prev = &(*prev)->next;
    }
}

// 'head' can be of '(const) EntryPtr&', too.
void
traverseList(EntryPtr head) {
    for (EntryPtr ep = head;
         ep;
         ep = ep->next)
    {
        cout << ep->value << endl;
        if (ep->next) {
            cout << ep->next.get_offset() << endl;
        }
    }
}

以上の道具を利用して、共有メモリ上に10エントリから成るリストを作って、それを子プロセスに手繰らせて削除、ということをするテストアプリケーションは以下。リストの先頭を指すポインタ(offset_ptr)自体も共有メモリ上に格納し、それには”head”という名前を付けて子プロセスから参照できるようにしている。子プロセスは”head”だけわかればあとはoffset_ptrを普通のポインタと同じように使って仕事をすればよい。

int
main(int argc, char* argv[]) {
    if (argc == 1) {
        // parent process: create a shared memory region and build
        // a linked list there.
        shared_memory_object::remove("TestSharedMemory");
        managed_shared_memory segment(open_or_create,
                                      "TestSharedMemory",
                                      4096);
        EntryPtr* list_head =
            segment.construct<EntryPtr>("head")(
                static_cast<Entry*>(NULL));
        for (int i = 0; i < 10; ++i) {
            addListEntry(segment, list_head, i);
        }
        system((string(argv[0]) + " child").c_str());
    } else {
        // child process: open the created shared memory and iterate over
        // the linked list stored there.
        managed_shared_memory segment(open_only,
                                      "TestSharedMemory");
        EntryPtr* list_head =
            (segment.find<EntryPtr>("head").first);
        assert(list_head != NULL);
        traverseList(*list_head);
        for (int i = 0; i < 10; ++i) {
            removeListEntry(segment, list_head, i);
        }
        segment.destroy_ptr(list_head);
        assert(segment.all_memory_deallocated());
    }
}

同様のことをmapped fileでやるのが以下の例。この例では、前半でmap用の新しいファイルを作ってそれをメモリにマップし、その上にリストを生成。後半では改めてそのファイルをオープンしてメモリにマップし、それを舐めた上で削除している。前半がifの中にあるのは念のため最初のmanaged_mapped_fileが破棄されるようにするため。また、前半部分の最後でflush()を呼んでメモリの内容がファイルに書き出されるようにしている(managed_mapped_fileのデストラクタあたりでこの処理をやってくれるのかどうか調べてないが、いろいろ試していた段階ではflush抜きだと動かない場合もあったような)。

int
main(int argc, char**) {
    if (true) {
        file_mapping::remove("TestMappedFile");
        managed_mapped_file segment(open_or_create,
                                    "TestMappedFile",
                                    4096);
        EntryPtr* list_head =
            segment.construct<EntryPtr>("head")(
            static_cast<Entry*>(NULL));
        for (int i = 0; i < 10; ++i) {
            addListEntry(segment, list_head, i);
        }
        segment.flush();
    }

    // (re)map the file to memory, and examine it.
    managed_mapped_file segment =
        managed_mapped_file(open_only,
                            "TestMappedFile");
    EntryPtr* list_head =
        segment.find<EntryPtr>("head").first;
    assert(list_head != NULL);
    traverseList(*list_head);
    for (int i = 0; i < 10; ++i) {
        removeListEntry(segment, list_head, i);
    }
    segment.destroy_ptr(list_head);
    assert(segment.all_memory_deallocated());
}

さらに、offset_ptr自体は共有メモリ型の領域でなくても動くので、普通にnew/deleteするメモリ管理で同じことをやってみたのが以下の例。

まず、Boostのmanged_xxx型を想定したテンプレートの条件を満たすようにwrapperのクラス(と、それを使ったEntry用のallocateメソッド)を定義する。

class NormalMemorySegment {
public:
    NormalMemorySegment() : allocated_(0) {}
    template <typename PointedType>
    void destroy_ptr(PointedType* ptr) {
        assert(allocated_ >= sizeof(PointedType));
        delete ptr;
        allocated_ -= sizeof(PointedType);
    }
    bool all_memory_deallocated() const { return (allocated_ == 0); }
    void addAllocated(size_t allocated_size) {
        allocated_ += allocated_size;
    }
private:
    size_t allocated_;
};

template <>
Entry*
Entry::allocate<NormalMemorySegment>(
    NormalMemorySegment& segment, int i)
{
    Entry* ptr = new Entry(i);
    segment.addAllocated(sizeof(Entry));
    return (ptr);
}

メインのテストコードは以下。もちろん、この場合は複数プロセス間でイメージを共有したりファイルに書き出したりとかいったことはできない。リストのヘッドを指すポインタは単にnewで確保して最後にdeleteしている。

int
main() {
    NormalMemorySegment segment;

    EntryPtr* list_head = new EntryPtr(NULL);
    for (int i = 0; i < 10; ++i) {
        addListEntry(segment, list_head, i);
    }
    traverseList(*list_head);
    for (int i = 0; i < 10; ++i) {
        removeListEntry(segment, list_head, i);
    }
    delete list_head;
    assert(segment.all_memory_deallocated());
}

この例の場合は、EntryPtrを単にEntry*と定義しても(ごくわずかな修正だけで)問題なく動作する。通常のアプリケーションで使う分にはほとんどポインタ型と同じだと思ってコードを書いても動きそう。

ということで、offset_ptrはなかなかよくできているという印象。某プロジェクト的には、とりあえず実際のメモリ管理にはnew/deleteを使いつつ、ポインタまわりはすべてoffset_ptrを使う形ではじめて、もう少し後で時間ができたら共有メモリ型に移行、というのがよさそう。これだとメインのコード(とくにread onlyの方)はほとんど変えずに、”head”に相当する部分だけをmanaged_shared_memoryとか使って取り出すようにするだけで動くと期待できる。

ただし、stackoverflowで議論されているように、offset_ptrにはデザイン上ちょっと問題といえる部分もある。NULLポインタアクセスがsegmentation faultにならない(上に、書き込みの場合は任意の領域が破壊されてしまう)というのは確かにやっかいかとは思うけど、ポインタを渡すところで偏執狂的にassert()するとかすればかなりカバーできるのではないかと思う。もしくは、性能とのトレードオフになるけど、offset_ptrの定義をコピってきてoperator->とかoperator*のところでNULLチェックするという手もある。

コメントを投稿 / Submit Comments


Warning: Undefined variable $user_ID in /usr/home/jinmei/src/itheme/comments.php on line 74



(あれば / Optional):