Containers with Stateful Allocator, a Bug in C++98
Posted on June 8th, 2008 at 15:02 by fr3@K

前一陣子花了些時間寫一個 allocator library. 首要目標就是要能符合 C++ Standard 對 allocator 定義的 requirement. 當然也要相容於 Standard C++ Library 的 container 實作, 能與所有的 container function 正確的 inter-operate. 在這個過程中, 我意外地一腳踏進又一個 C++ 的陰暗角落.

先讓我把這個有點嚇人的結論寫在前面:

    你所使用的標準 container , 很可能無法完全正確地與 object-stateful1 的 allocator 一起運作. 即便該 allocator 確實是符合標準規範的.

不過先別慌. 只要拿來與 container 搭配使用的是沒有 per instance 狀態的 allocator2, 你就不會遭遇本文所討論的問題.


我在這個 pet project 裏面規劃了兩個 object-instance stateful 的 allocator class template. 第一個是專為 (在 function scope 內用過即丟) temporary object 的 allocation 設計並最佳化, 叫作 tmp_allocator. 第二種是 pool_allocator. 在 Boost 已經有 boost::pool_allocator 的情況下另外做一個 memory pool allocator 的原因是 boost::pool_allocator 其實只不過是 boost::singleton_pool 的一個 adapter/wrapper. 也就是說它是沒有 per instance 狀態的. 而這對無法滿足我設定的一些使用狀況, 我希望相同類別的 container instance 可以使用不同 (memory pool) allcoator instance. 當然, 這些 (現在知道) 無法實際應用的規劃都是在我了解這篇文字要說明的問題之前就已經做好的.

先讓我簡單解釋一下 container 與 allocator 之間的關係以及兩者的相關部份特性.

每一個 container instance 都擁有一個 allocator instance.3 從 C++98 可以清楚了解 allocator 是被設計成可以有 per instance 狀態的. 譬如 lib.allocator.requirements 要求能夠比較同一類別的 allocator instance 的 equality 與 inquality. 更精確的說, 兩個 alloctor instance 的 equality/inequality 比較是在測試從 allocator 1 得到的 memory 是否可以拿到 allocator 2 去釋放. 在 lib.containers 也可以看到, 所有 container 的每一個 constructor 都接受 allocator 當參數.4 Container 在建構的時候都會複製外部傳進來的 allocator. 這隱含了 allocator 的 per instance 狀態不能是直接的 (or 不該是直接的?). 我想沒有一個神智清楚的人會把 allocator 設計成直接管理記憶體, 而導致 allcoator 在 copy-construct 的時候必須把內容 deep copy 到另一個 allocator. 因此, allocator 可以有的 per instance 狀態該是間接的. 一種合理的設計是在 allocator 下層另有某種 memory manager 的機制. 不同的 allocator instance 可以參考到同一個 memory manager, 也可以參考到不同的 memory manager. 於是複製有 per instance 狀態的 allocator 就等同於創建多一個參考到同一個 memory manager 的 allocator instance. 這又間接表示了同一類別的 container instance 可以各自 (間接) 使用不同的 memory manager.

問題來了. 兩個相同類別的 container instance, c1 and c2, swap 是怎麼實作的? 特別是在兩個 container instance 擁有的 allocator 不相等時 (i.e. c1.get_allocator() != c2.get_allocator()), 是怎麼 swap 的?

    = mini tutorial =

    典型的 vector implementation 內部會有三個 member data. 分別指向 vector instance 所管理的連續 object array 的頭尾以及已 allocate 了但目前不是使用中的記憶體末端.

    0 ~ 4: managed elements
      x  : allocated but unused memory blocks
    +---+---+---+---+---+---+---+---+....
    | 0 | 1 | 2 | 3 | 4 | x | x | x |   :
    +---+---+---+---+---+---+---+---+....
      ^                   ^           ^
      |                   |           |
    start              finish     end of storage
    

    = end mini tutorial =

vector 為例. 先看 GCC default 搭配的 libstdc++ 是怎麼做的:

template<typename _Tp, typename _Alloc>
void
vector<_Tp, _Alloc>::swap(vector& __x)
{
  std::swap(this->_M_impl._M_start, __x._M_impl._M_start);
  std::swap(this->_M_impl._M_finish, __x._M_impl._M_finish);
  std::swap(this->_M_impl._M_end_of_storage,
            __x._M_impl._M_end_of_storage);
}

不考慮 allocator 就直接 swap 這三個 pointer 可是會造成很大的問題. 在 c1.swap(c2) 時, 兩個 container instance 所管理的 memory 可以是從兩個不同的 stateful memory manager 那邊 allocate 到的, 比方說兩個不同的 memroy pool. 將從 memory pool 1 所 allocate 到的 memory 釋放到 memory pool 2 去, 迎接你的很可能不是 assertion failure (如果有燒香的話, 否則) 就是 undefined behavior, 很難期待會有好的結果.

再來看看另外兩個類似的 implementation. VC++ 2005 (from DINKUMWARE):

template<class _Ty,
  class _Ax>
void vector<_Ty, _Ax>::swap(_Myt& _Right)
  { // exchange contents with _Right
  if (this->_Alval == _Right._Alval)
    { // same allocator, swap control information

#if _HAS_ITERATOR_DEBUGGING
    this->_Swap_all(_Right);
#endif /* _HAS_ITERATOR_DEBUGGING */

    std::swap(_Myfirst, _Right._Myfirst);
    std::swap(_Mylast, _Right._Mylast);
    std::swap(_Myend, _Right._Myend);
    }
  else
    { // different allocator, do multiple assigns
    _Myt _Ts = *this; *this = _Right, _Right = _Ts;
    }
  }

以及由 Rogue Wave donate 給 ApacheSTDCXX:

template <class _TypeT, class _Allocator>
void
vector<_TypeT, _Allocator>::swap (vector& _Right)
{
  if (get_allocator () == __other.get_allocator ()) {
    pointer __tmp    = _C_begin;
    _C_begin         = __other._C_begin;
    __other._C_begin = __tmp;
    __tmp            = _C_end;
    _C_end           = __other._C_end;
    __other._C_end   = __tmp;
    __tmp            = _C_bufend;
    _C_bufend        = __other._C_bufend;
    __other._C_bufend = __tmp;
  }
  else {
    // not exception-safe
    _C_unsafe_swap (__other);
  }
}

這兩個 implementation 的 swap 基本上是相同的, 都考慮到了 allocator 的 equality. 在兩個 allocator instance 相同的時候都是 swap 三個 pointer. 在不相等的時候則都是用到了一個 temporary 的 vector instance, 做一次 object level 的淺層 swap (e.g. T tmp(*this); *this = other; other = tmp;). 這個策略在 allocator 不相等時的確避開了 libstdc++ 的困境, 但卻引進了另外兩個問題. 一是把本該是 O(1) 的 swap 變成 O(N), swap 不再一定是絕對輕鬆的操作. 另一個是問題是這種 swap 將不再具有 no-throw 的 exception-safety 保證. 這兩個改變 (IMO, 非常糟糕地) 推翻很多用到 swap 的 algorithm 所做的假設或期望, even within the STL itself!!!

C++ Standard Library Active Issues List 的 issue 431 如此寫道:

431. Swapping containers with unequal allocators

Section: 20.1.5 [lib.allocator.requirements], 25 [lib.algorithms] Status: Open Submitter: Matt Austern Date: 20 Sep 2003

Clause 20.1.5 [lib.allocator.requirements] paragraph 4 says that implementations are permitted to supply containers that are unable to cope with allocator instances and that container implementations may assume that all instances of an allocator type compare equal. We gave implementers this latitude as a temporary hack, and eventually we want to get rid of it. What happens when we’re dealing with allocators that don’t compare equal?

根據這段文字, Standard Library 的 implementaion 可以不具備處理本文討論的 allocator instance 不相等的能力, 並允許 implementation 假設它們都相同. 這是個在計畫中將會被移除, 一個給實作者方便的暫時性 hack.

IMHO, 這個方便讓整個 container/allocator 架構跛了腳. 沒有辦法更彈性地將具有特別功能的 allocator 搭配 container 使用, i.e. special purpose allocators those are object stateful.

看到這裡可能已經有人想到, 如果在 swap 的時候不只 swap vector 的三個 pointer, 把 vector 的 allocator instance 也一併 swap 過去就可迴避掉前面提到的兩種實作遭遇到的麻煩. 偏偏 C++08 並不要求 allocator 俱備被 swap 的能力.5 也就是說不能被 swap 的 (user-defined) allocator 也是完全符合規範的. 我猜就是因為這個原因, 使得這些 implementaion 寧願將就 non-swappable (user-defined) allocator,6 而不願意採用這個策略實現 container 的 swap function, 即便 std::allocator 是完全可以被 swap 的.

最後, 我們來看 STLportvector::swap:

template <class _Tp, class _Alloc>
void vector<_Tp, _Alloc>::swap(__Self& __x) {
  _STLP_STD::swap(this->_M_start, __x._M_start);
  _STLP_STD::swap(this->_M_finish, __x._M_finish);
  this->_M_end_of_storage.swap(__x._M_end_of_storage);
}

乍看之下這個 implementation 似乎跟 libstdc++ 的實現是類似的, 往下鑽進去會發現完全不是那麼一回事. 它 swap 的第三個 member data, _M_end_of_storage, 的類別跟其他 implementation 不同, 不是個 pointer. 是一個類別為 _STLP_alloc_proxy<pointer, _Tp, allocator_type> 的 object instance. _STLP_alloc_proxy 提供了兩個功能, pointer to end of storage, 以及針對沒有 per instance 狀態的 allocator 做的 Empty Base Optimization. 所以當 _M_end_of_storage 被 swap 的時候, 是連 allocator 也一起 swap 的. 就這個功能來說 STLport 的實現是最合乎 real world 的真實狀況, 卻必須付出不符合標準的代價, 因為無法與不能被 swap 但符合標準規範的 allocator 一起運作.

從這篇文字討論的問題可以發現, 標準並不一定是完善的. 雖然 遵從標準的規範 通常個好習慣, 但重視標準甚於實際的應用卻是要不得的. 還不確定這個 issue 未來的發展會是如何, 只知道已經進了 C++ 委員會的 issue list, 期望能在 C++0x 得到修正. 讓 STLport 的實現符合標準, 更鼓勵其他的 implementation 也轉而採取更實用也更安全的實現策略.

[Update]

原本我是以理解的角度來看待 VC++ 2005 的 C++ Standard Library 與 STDCXX 這兩個實作為了符合 C++98 的標準所做的妥協.

但想到標準沒有要求 allcator 能夠被繼承 (至少我找不到相關的文字敘述). 也就是說一個 不能被繼承 (另一篇討論這個技巧的文章) 的 allocator class 仍舊可以是符合標準的.

STDCXX 使用了 Empty Base Optimization 這個優化, 讓 vector 繼承自 allocator. 換句話說使用者不能拿一個符合標準但不可以被繼承的 allocator 來與 STDCXX 的 container 一起使用. 因此, STDCXX 是不符合標準規範的.

沒想到檢視一個小小的 vector::swap 所得到的結果, 竟然是 MS 的產品最符合標準.

ps. 本篇談到的四個 implementation 之中, 只有 VC++ 的 container 沒有對 allocator 施以 Empty Base Optimization.

[References]


  1. 每個同類別的 object instance 可以有各自不同的狀態. 例如 non-static member data []
  2. 例如 std::allocatorBoost.Poolpool_allocator 以及 fast_pool_allocator 都是沒有 per instance 狀態的 allocator []
  3. 有些 stdlib 的 implementaion 在 contaner 使用沒有 per instance 狀態的 allocator 時, 會最佳化成所有相同類別的 container instance 都使用同一個 allocator instance, 以減少 container instance 的 memory foot print []
  4. 由於 container 內嵌 allocator, 因此看似沒有 allocator 當參數的 copy-constructor 其實也隱性地傳遞了 allocator []
  5. 一個方法是在 allocator 的 namespace 內定義 free function 版本的 swap(allocator&, allocator&). 另一種作法是讓 allocator (在已有 copy ctor 的前提下) 具有被指派 (assignment operator) 的能力 []
  6. 在實踐中, 我從沒見過 non-swappable allocator []
del.icio.us:Containers with Stateful Allocator, a Bug in C++98 digg:Containers with Stateful Allocator, a Bug in C++98 spurl:Containers with Stateful Allocator, a Bug in C++98 newsvine:Containers with Stateful Allocator, a Bug in C++98 furl:Containers with Stateful Allocator, a Bug in C++98 Y!:Containers with Stateful Allocator, a Bug in C++98 黑米共享書籤:Containers with Stateful Allocator, a Bug in C++98 推推王:Containers with Stateful Allocator, a Bug in C++98
Previous Post
« Why Const Matters «
Next Post
» Live (MS) Search Spam »

5 Comments »

Comment #4696

Allocator per instance is used for performance generally.
The issues you raise is not real problem you gonna happened, unless
you don’t know how the whole idea works.

If you concern too much , nothing will be done before everything is perfect.

It’s not reasonable to exchange data between two same class object by different
allocator. It won’t and should’nt be happened in real work. Unless the coder is idiot.

Comment by Ziyu Huang — October 9, 2008 @ 7:11


Comment #4699

Hi Ziyu,

For many C++ programmers, this post is just flat boring, most of them probably couldn’t care less about the issues it attempted to highlight. I am thrilled by the fact that there are real people who read this post.

However, I failed to understand some of your comment in a sensible way. My guess is that, it would be easier if we could communicate in Chinese, regarding this post. :) (But I could be wrong :P)

Allocator per instance is used for performance generally.

Yes and No.

我想你的原意與 STL-style allocator 無關, 指的是特別的 custom allocator.

我舉個例, 除了 general purpose memory allocator (e.g. C 的 malloc, C++ 的 ::operator new), 最常見的 memory allocator 大概就是 memory pool (e.g. boost::singleton_pool). Memory pool 會被使用的原因很多, 當然 runtime performance 可以是原因, 也有其他與 runtime performance 無關的重要因素會讓 programmer 選擇 memory pool. 例如為了確保 memory allocation 不會失敗 (e.g. pre-allocation), 或是限制某 module 使用 memory 的上限 and/or 簡化 garbage colloction (e.g. resource pool).

The issues you raise is not real problem you gonna happened, unless you don’t know how the whole idea works.

這句話前半句的意思是說 “這不是個有意義的問題, 因為你不會遇到這個狀況” 嗎???

事實上我就是遇到了這個狀況. 雖然鑽研 STL source code 常是有趣的事情, 但這次我是在對 container 做 swap 時觀察到預期之外的行為才發現這個問題的.

後半句好像是說 “除非你不完全清楚這玩意到底是怎麼運作的”.

小弟應該算是稍微清楚的…

If you concern too much , nothing will be done before everything is perfect.

我猜這句話的意思是 “想太多會幹不了事, 沒有東西是完美的”.

的確, 我是個想多的人 :P 不過老闆還沒把我 fire 掉, 還是有在幹事啦~~

C++ 絕對不是最好或是 perfect. 我也不敢期盼任何事物是完美的. (除了老婆之外 :P) 但請你考慮一下我的角度, 我 (自認) 是個熱愛 C++ 的重度使用者, 如果你也看了我其他的文字的話可能會注意到, 雖然在評論時我會試著客觀, 但在模糊地帶我只會偏心說 C++ 的好話, 幹樵都是樵別人. 在我跳出來談 C++ 語言/library/standard 的缺點時, 雖不見得是天大的事情, 卻 (IMO) 很可能是值得探討/注意的 issue.

It’s not reasonable to exchange data between two same class object by different allocator. It won’t and should’nt be happened in real work. Unless the coder is idiot.

很遺憾, 我對這段的理解比較肯定. 這個笨蛋就是我 XD. 不論我同不同意這個結論, 希望 Ziyu 可以對前題 - 為什麼這在真實世界不會發生 - 做進一步的說明.

BTW, I didn’t feel offended by your comment. 但我確實希望能夠了解你的回應中我沒能夠清楚理解的部份.

Cheers~

Comment by fr3@K — October 9, 2008 @ 18:48


Comment #4700

In my works , allocator is used for make sure memory can be safely free
and don’t need to call destructor on objects and help performance for huge
number of objects. So, I my works, it is almost for performance.

Therefore, I will carefully design all classes won’t mess up when
we we destory allocator. So, if we use objects along with particular
allocator, these objects will just like living in isolated world.

If you can’t isolate them, you shouldn’t use allocator.
I don’t believe there is any kind allocator will allow you swap managed memory
block.

STDCXX is good enough to protect you do stupid things, in my opinion.

Comment by ziyu_huang — October 10, 2008 @ 20:18


Comment #4701

BTW, I don’t belive my C++ is better than you.
I just feel strange you try to figure out what will happen
when STL objects works with mixed allocator.
I my experience, even not STL. this is not allowed.

Comment by ziyu_huang — October 10, 2008 @ 20:29


Comment #4703

Hi Ziyu,

It seems to me, in the context of your comments regarding this post, the word “allocator” is not related to STL allocator. You just meant a software module which enables dynamic memory allocation in a more generic sense.

If so, I think I’ve managed to understand your comment better this time. :)

In this reply, I am going to use the word “allocator” the same way as you do, unless otherwise specified.

In my works , allocator is used for make sure memory can be safely free and don’t need to call destructor on objects and help performance for huge number of objects…

IMO, this is a customized garbage collection scheme/protocol for this special purpose non-global (process wise) allocator, performance gain is one of its side effects.

Therefore, I will carefully design all classes won’t mess up when
we we destory allocator. So, if we use objects along with particular
allocator, these objects will just like living in isolated world.

I gotcha. Your schema works similar to allocating memory blocks in a process, without deallocation. However, they would be deallocated when the process exists anyway.

Your schema is basically the same as boost::pool (non-typed) and boost::object_pool (typed).

Correct?

If you can’t isolate them, you shouldn’t use allocator.
I don’t believe there is any kind allocator will allow you swap managed memory block.

I disagree. This is just a schema one (in this case, you) chooses to use in his/her application. Though it may be one of the better schema for that particular application.

However, one may adopt a different schema (say) so that an allocator is never destroyed earlier than any reference to its managed memory blocks, for example, boost::singleton_pool. This would’ve surrendered the isolation you deemed mandatory pointless, in a different application where a different choice was made.

Always keep this in mind, there is no one schema that works best for all applications.

STDCXX is good enough to protect you do stupid things, in my opinion.

Okay, back to STL allocators. Yes, functionality wise, STDCXX works. (Was it your intention to not mention DINKUMWARE/MSVC? The behaviors of the two implementations are effectively the same in this regard.) In fact, according to the Standard, all implementations I examined worked (functionality wise) and are compliant to the Standard. (that is if we don’t take the non-inheritable allocator thing into account) But this post is not about functionality nor Standard compliance. Rather, it’s about stateful STL allocator semantics and assumptions programmers make about STL containers (in respect of swapping, mostly). For example, one may write a function template that needs to swap the content of two containers in the middle of things it does:

template <class StlContainer>
void foo(StlContainer& container)
{
  // do something meaningful...

  // This container, `another_container`, uses default
  // constructed instance of type StlContainer::allocator_type.
  StlContainer another_container;

  // However, since the parameter `container` is passed in, it
  // could very well use an user specified instance of type
  // StlContainer::allocator_type.
  swap(another_container, container);

  // do other meaningful things...
}

One would (I certainly would before I wrote the allocator library mentioned earlier in this post) most likely to assume the swapping is always trivial (say swapping a few pointers) and no-throw. When such assumption is more than programming convenience, but an assertion he/she relies on for a particular application, he/she may be in very deep trouble.

Because there is nothing stopping users of this function passing a STL container which uses a stateful allocator. BTW, IMO, such usages are very valid and reasonable, unless it is documented otherwise. To make things worse, this is not an issue caused by the STL implementation he/she uses, the problem roots in the Standard. He/she can’t just file a bug report and expect someone would fix the implementation.

BTW, I don’t belive my C++ is better than you.

This I wouldn’t know and is a non-matter in this discussion.

Anyhow, I know no shame so I took it as a compliment. Thanks.

I just feel strange you try to figure out what will happen
when STL objects works with mixed allocator.
I my experience, even not STL. this is not allowed.

Yes, it is allowed, at least in the context of STL containers. It is a real problem. Please visit the first two links in the reference section of this post:

As you can see, this issue has been noted and documented more than once, by the C++ Standard Committee. I am not the first person to talk about this issue, I just happen to re-discovered it on my own.

Cheers~

PS. this has gotta be one of the longest replies I’ve ever written.

Comment by fr3@K — October 11, 2008 @ 23:05


Comments RSS TrackBack URI

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>