SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

「std::vector」観察記録
~慣れ親しんだ可変長配列の仕組みとふるまいを検証してみた

メモリーの使われ方

 vector内の各要素は連続、すなわちvector<T> cに対して&c[n] + 1 == &c[n+1]であることが保証されています。それ故vectorの容量を拡張する際に増加分を継ぎ足すことができません。おうちが手狭になっても増築は許されず、より大きなおうちを新築し/引っ越して/以前のおうちを解体するしかありません。その様子を観察しましょう。

 まず領域拡張の度合を調べます。vectorのメンバ関数size()とcapacity()はそれぞれ"要素数"と"拡張なしに収容できる要素の個数"を返します。vector<int>に100個分の容量を確保した状態から要素を1つずつ挿入し、capacity()が変化した(拡張が行われた)ときのsize()とcapacity()とを出力してみます。

VectorExample05.cpp
#include <iostream>
#include <vector>

using namespace std;

int main() {
  {
  cout << "\n----- vector<int> size & capacity" << endl;
  vector<int> c;
  c.reserve(100);
  size_t cap = c.capacity();
  for ( int i = 0; i < 1000; ++i ) {
    if ( cap != c.capacity() ) {
      cout << "size=" << c.size() 
           << "\tcapacity=" << (cap = c.capacity()) 
           << "\tchange ratio=" << static_cast<double>(c.capacity()) / c.size() 
           << endl;
    }
    c.emplace_back(i);
  }
  cout << "-----" << endl;
  }
}
fig05
fig05

 Visual C++ 12(Visual Studio 2013)のvectorはおおよそ5割増の領域拡張を行っています。そして要素数が100から1000になるまでに領域の確保/要素の移動/旧領域の解放を6回繰り返しています。なので最大要素数が想定できるのであれば、あらかじめreserve()しておけば少しは速くなりそうです。移動(コピー)がコスト高な要素を大量に挿入するような場合、かなり効果があるんじゃないかしら。

 つぎにメモリーの利用効率、要素の格納に必要な領域の大きさを実測します。vectorに限らずSTLコンテナはメモリーの確保/解放を司る部分を外付けにできますから、これを利用して確保/解放のたびにどれだけ取ったか/返したかを出力しましょう。

mallocator.h
#ifndef MALLOCATOR_H__
#define MALLOCATOR_H__

#include <cstdio>
#include <cstdlib>
#include <new>
#include <map>
#include <typeinfo>

namespace epi {

  template<typename T> struct mallocator {
    typedef T value_type;
    template<typename U> mallocator(const mallocator<U>&) {}

    mallocator() {}
    bool operator==(const mallocator&) const{ return true;  }
    bool operator!=(const mallocator&) const{ return false; }

    T* allocate(const size_t n) const {
      if ( n == 0 ) 
        return nullptr;
      if (n > static_cast<size_t>(-1) / sizeof(T) )
        throw std::bad_array_new_length();
      size_t siz = n * sizeof(T);
      void* pv = std::malloc(siz);
      if ( !pv )
        throw std::bad_alloc();
      usage_[pv] = siz;
      printf("%3d %s (%4dB) MALLOC : %p (%d)\n", n, typeid(T).name(), siz, pv, in_use());
      return static_cast<T*>(pv);
    }

    void deallocate(T* const p, size_t n) const {
        usage_.erase(p);
        printf("%3d %s (%4dB) FREE :   %p (%d)\n", n, typeid(T).name(), sizeof(T)*n, p, in_use());
        std::free(p);
    }

    void construct(T* p, const T& val) { new ((void*)p) T(val); }

    void destroy(T* p) { p->T::~T(); }

    template<typename Other> struct rebind {
        typedef mallocator<Other> other;
    };

    static std::map<void*,size_t> usage_;
    static size_t in_use() {
      size_t result = 0;
      for ( auto item : usage_ ) {
        result += item.second;
      }
      return result;
    }
  };

template<typename T> std::map<void*,size_t> mallocator<T>::usage_;

}

#endif

 vectorとlistでメモリーの使いっぷりを観察した結果がコチラ。

VectorExample06.cpp
#include <iostream>
#include <vector>
#include <list>

#include "mallocator.h"

using namespace std;
template<typename T> using my_vec  = std::vector<T, epi::mallocator<T>>;
template<typename T> using my_list = std::list<T, epi::mallocator<T>>;

int main() {

  {
  cout << "\n----- vector<int>" << endl;
  my_vec<int> c;
  for ( int i = 0; i < 10; ++i ) {
    c.push_back(i);
  }
  cout << "-----" << endl;
  }
  
  {
  cout << "\n----- list<int>" << endl;
  my_list<int> c;
  for ( int i = 0; i < 10; ++i ) {
    c.push_back(i);
  }
  cout << "-----" << endl;
  }

}
fig06
fig06

 vector<int> での結果から、前述の新築と解体の様子が見てとれます。新旧領域間での要素の移動の間、新旧2つの領域が存在するため、一時的に全要素が必要とする領域のおよそ2.5倍が確保されていますが、定常的には要素1つ分の大きさ×要素数×(1~1.5)、おおむね最小限のメモリー量でやりくりしてくれてます。

 対してlist<int>では要素の型つまりintではなく、std::_List_node<int,void*>が各要素の格納のために確保されています。この_List_node<>の正体をヘッダ<list>に見つけました。

_List_node
template<class _Value_type>
struct _List_node<_Value_type,void*> {
  typedef _List_node<_Value_type,void*>* _Nodeptr;
  _Nodeptr _Next;
  _Nodeptr _Prev;
  _Value_type _Myval;
private:
  _List_node& operator=(cosnt _List_node&);
};

 要素1つを格納するのに、その大きさに加えてポインタ2つ分大きい領域を消費します。listは双方向のリンク・リストなので直前と直後の要素をたどるため2つのポインタが必要なんですね。なので要素が小さいほどメモリ効率は相対的に悪くなります。このサンプルは32bitアプリとしてコンパイルしていますけど、64bitだとポインタが大きくなって要素1つあたり16byte余計に必要ってことになります。

次のページ
処理速度

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/7849 2014/07/23 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー