{"id":64,"date":"2017-04-07T15:54:33","date_gmt":"2017-04-07T19:54:33","guid":{"rendered":"https:\/\/cyberbisson.com\/blog\/?p=64"},"modified":"2021-07-22T21:36:36","modified_gmt":"2021-07-23T01:36:36","slug":"an-exploration-of-the-deleter-on-the-memory-footprint-of-stdunique_ptr","status":"publish","type":"post","link":"https:\/\/cyberbisson.com\/blog\/2017\/04\/07\/an-exploration-of-the-deleter-on-the-memory-footprint-of-stdunique_ptr\/","title":{"rendered":"An Exploration of the Deleter on the Memory Footprint of <tt>std::unique_ptr<\/tt>"},"content":{"rendered":"<p><!-- Title: An Exploration of the Deleter on the Memory Footprint of <tt>std::unique_ptr<\/tt>\n  -- Categories: Software\/C++\n  -- Tags: c++ deleter gcc programming software tuple unique_ptr\n  -- FB Open Graph: https:\/\/cyberbisson.com\/editorial\/unique_handle.png --><\/p>\n<div class=\"alignright right\">\n<img decoding=\"async\" src=\"\/editorial\/unique_handle.png\">\n<\/div>\n<p>(\u2026or \u201cHow I Learned to Love the Tuple.\u201d)<\/p>\n<p>Although the C++ standard does not explicitly spell it out, one may hope for a sophisticated implementation of the <tt>std::unique_ptr<\/tt> that consumes exactly as much memory as the pointer it tracks when it is configured to use a empty-class deleter (such as the default deleter type).  The <tt>unique_ptr<\/tt> indeed allows the consumer to supply a custom deleter, which one intuitively expects to consume memory\u2009\u2014\u2009if you store two pieces of data, you will consume two spots in memory somehow, right?  Not quite.  I will explore how this is done (specifically in GCC).<\/p>\n<p><!--more--><\/p>\n\n<h1> A Na\u00efve Implementation<\/h1>\n<p>Once upon a time, I wrote a handle container type, similar to <tt>unique_ptr<\/tt>.  In fact, I called this <tt>unique_handle<\/tt> (you can see it <a href=\"https:\/\/cyberbisson.com\/pstack-2.0\/classpsystem_1_1unique__handle.html\">here<\/a>).  Because I wanted to optimize for my most common case\u2009\u2014\u2009a simple <tt>CloseHandle()<\/tt>\u2009\u2014\u2009I optimized the container as suggested above.  If the caller used the default deleter, no <tt>unique_handle<\/tt> instance should consume more memory than the handle itself.<\/p>\n<p>I achieved this behavior with template specialization.  The generic <tt>unique_handle<\/tt> class template had storage for the deleter alongside the handle itself.  I then provided a specific specialization for <tt>unique_handle<\/tt> that was given the default deleter (specified as a default template parameter).  Unfortunately, specializing a class requires duplication of various bits of code, so the <tt>unique_handle<\/tt> type had a base class for common code, and two user-visible implementation types, adding to the overall complexity.<\/p>\n<p>Another downside to this approach is the fact that <em>only<\/em> the recognized deleter type resulted in a smaller footprint.  Even using a deleter derived from the default one resulted in increased size.<\/p>\n<h1>What GCC Produces<\/h1>\n<p>Our wishes become reality, because I can show that <tt>unique_ptr<\/tt> <em>does<\/em> consume the minimum possible size in GCC, as I suggested a \u201cgood\u201d implementation would.  Consider the following test code:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/\/ Here is the memory we will track with std::unique_ptr.\nstruct blob {\n    int something = 0;\n};\n\n\/\/ This is a deleter that takes no space.\nstruct delete_blob_empty {\n    void operator()(blob *const in) const noexcept {\n        delete in;\n    }\n};\n\n\/\/ This deleter has state -- essentially, should we zero out the pointer after\n\/\/ deletion.\nstruct delete_blob {\n    bool null_it = false;\n    void operator()(blob *in) const noexcept {\n        delete in;\n        if (null_it) in = nullptr;\n    }\n};\n\nint main()\n{\n    delete_blob my_deleter;\n    delete_blob_empty my_empty_deleter;\n\n    std::unique_ptr&lt;blob&gt; some_blob(new blob);\n    std::unique_ptr&lt;blob, delete_blob&gt; custom_blob(new blob, my_deleter);\n    std::unique_ptr&lt;blob, delete_blob_empty&gt; empty_custom_blob(\n        new blob, my_empty_deleter);\n\n    std::cout &lt;&lt; \"Sizes: some_blob = \"    &lt;&lt; sizeof(some_blob)\n              &lt;&lt; \"; custom_blob = \"       &lt;&lt; sizeof(custom_blob)\n              &lt;&lt; \"; empty_custom_blob = \" &lt;&lt; sizeof(empty_custom_blob)\n              &lt;&lt; '\\n';\n\n    std::cout &lt;&lt; \"Done!\\n\";\n    return 0;\n}\n<\/pre>\n<p>Which prints:<\/p>\n<pre>Sizes: some_blob = 8 custom_blob = 16 empty_custom_blob = 8\nDone!\n<\/pre>\n<p>Great!  The only <tt>unique_ptr<\/tt> that consumes 16 bytes (this is a 64-bit system, so the size of a pointer will be 8 bytes) is the one that contains a custom deleter that has state (<tt>delete_blob<\/tt>).  This means that not only have they addressed the default deleter case, as I had in <tt>unique_handle<\/tt>, but they even detect custom deleters that don\u2019t consume space, and react accordingly.<\/p>\n<h1>How Is It Implemented?<\/h1>\n<p>How is GCC doing this?  It certainly isn\u2019t in the same manner as my earlier suggestion.  A quick search of the <tt>libstdc++<\/tt> headers (specifically, <tt>bits\/unique_ptr.h<\/tt>) shows that there is no specialization based on the deleter.  Actually the only specialization of <tt>unique_ptr<\/tt> is to support different behavior when <tt>_Tp<\/tt> (the pointer type) is a simple pointer, versus an array.  These <tt>unique_ptr<\/tt> implementations are quite succinct, and do not delegate any responsibility to implementation types.  Here\u2019s what we have:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/\/\/ 20.7.1.2 unique_ptr for single objects.\ntemplate &lt;typename _Tp, typename _Dp = default_delete&lt;_Tp&gt; &gt;\n  class unique_ptr\n<\/pre>\n<p>\u2026and:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/\/\/ 20.7.1.3 unique_ptr for array objects with a runtime length\n\/\/ [unique.ptr.runtime]\n\/\/ _GLIBCXX_RESOLVE_LIB_DEFECTS\n\/\/ DR 740 - omit specialization for array objects with a compile time length\ntemplate&lt;typename _Tp, typename _Dp&gt;\n  class unique_ptr&lt;_Tp[], _Dp&gt;\n<\/pre>\n<h2>Enter the <tt>std::tuple<\/tt><\/h2>\n<p>The secret to all of this is how <tt>unique_ptr<\/tt> stores its data.  Back in our na\u00efve implementation, we would have simply defined two fields, one for the pointer data, and a second for the deleter.  This is not what GCC does.  In the declaration for <tt>unique_ptr<\/tt>:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">typedef std::tuple&lt;typename _Pointer::type, _Dp&gt;  __tuple_type;\n__tuple_type                                      _M_t;\n<\/pre>\n<p>The compiler will reduce this down ultimately to:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">std::tuple&lt;blob*, std::default_delete&lt;blob&gt;&gt; _M_t; \/\/ For some_blob\nstd::tuple&lt;blob*, delete_blob&gt; _M_t;               \/\/ For custom_blob\nstd::tuple&lt;blob*, delete_blob_empty&gt; _M_t;         \/\/ For empty_custom_blob\n<\/pre>\n<p>So this means the magic is actually somewhere in <tt>std::tuple<\/tt>.  This implication is great, and it means that not only are the entire class of <tt>unique_ptr<\/tt> instances memory optimized to store only what\u2019s required, but <em>anything that uses <tt>std::tuple<\/tt> automatically strips away types that require no in-memory footprint, while still giving you access to their facilities.<\/em>  I will restate this another way: we have asked to store two pieces of information, and we can still use all the functions of both pieces of data, but only one actually takes memory resources.  A simple <tt>struct<\/tt>, or even <tt>std::pair<\/tt> cannot make this promise.<\/p>\n<p>The <tt>std::tuple<\/tt> class template (found in the header file, <tt>tuple<\/tt>) consists of a recursively-defined variadic template that forwards to a type called <tt>_Tuple_impl<\/tt>.  Most of <tt>_Tuple_impl<\/tt>\u2019s purpose is to keep track of the position in the <tt>tuple<\/tt> of the current element, and to break off a type from the template parameter pack, handing it to <tt>_Head_base<\/tt>.<\/p>\n<p>Finally, with <tt>_Head_base<\/tt> we see some template specialization.  Here is the (empty) class template declaration.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">template&lt;std::size_t _Idx, typename _Head, bool _IsEmptyNotFinal&gt;\n  struct _Head_base;\n<\/pre>\n<p>Pay close attention to this third template parameter, <tt>_IsEmptyNotFinal<\/tt>.  <tt>_Head_base<\/tt> has these two specializations:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">template&lt;std::size_t _Idx, typename _Head&gt;\n  struct _Head_base&lt;_Idx, _Head, true&gt;\n  : public _Head\n  {\n    constexpr _Head_base()\n    : _Head() { }\n\n    constexpr _Head_base(const _Head&amp; __h)\n    : _Head(__h) { }\n\n    \/\/ ... [MB] Interface omitted ...\n  };\n\ntemplate&lt;std::size_t _Idx, typename _Head&gt;\n  struct _Head_base&lt;_Idx, _Head, false&gt;\n  {\n    constexpr _Head_base()\n    : _M_head_impl() { }\n\n    constexpr _Head_base(const _Head&amp; __h)\n    : _M_head_impl(__h) { }\n\n    \/\/ ... [MB] Interface omitted ...\n\n    _Head _M_head_impl;\n  };\n<\/pre>\n<p>Simply put, if we get an <tt>_IsEmptyNotFinal<\/tt> that is <tt>true<\/tt>, the class that\u2019s defined doesn\u2019t even have a data member.  For completeness, the <tt>std::tuple<\/tt> implementation sets this parameter by passing the pointer type to the metafunction, <tt>__empty_not_final<\/tt> (which is a topic for another day) but it essentially boils down to <tt>std::is_empty<\/tt>:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">template&lt;typename _Tp&gt;\n  struct __is_empty_non_tuple : is_empty&lt;_Tp&gt; { };\n\n\/\/ Using EBO for elements that are tuples causes ambiguous base errors.\ntemplate&lt;typename _El0, typename... _El&gt;\n  struct __is_empty_non_tuple&lt;tuple&lt;_El0, _El...&gt;&gt; : false_type { };\n\n\/\/ Use the Empty Base-class Optimization for empty, non-final types.\ntemplate&lt;typename _Tp&gt;\n  using __empty_not_final\n  = typename conditional&lt;__is_final(_Tp), false_type,\n                         __is_empty_non_tuple&lt;_Tp&gt;&gt;::type;\n<\/pre>\n<p><em>Note: If I make <tt>delete_blob_empty<\/tt> a <tt>final<\/tt> class, the <tt>tuple<\/tt> again takes a full 16 bytes.<\/em>  This restriction exists because <tt>_Head_base<\/tt> derives from the passed-in template type, so accepting the <tt>final<\/tt> type would result in compilation errors.<\/p>\n<h2>In The Debugger<\/h2>\n<p>To get a concrete feel for how this looks in memory, let\u2019s examine it in the debugger.  If I examine the <tt>unique_ptr<\/tt> instance (from the example code) named <tt>some_blob<\/tt>, I see this (I have manually pretty-printed GDB output a little more from the basic \u201c<tt>p \/r<\/tt>\u201d output):<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">&lt;std::_Tuple_impl&lt;0ul, blob*, std::default_delete&lt;blob&gt; &gt;&gt; = {\n\n    &lt;std::_Tuple_impl&lt;1ul, std::default_delete&lt;blob&gt; &gt;&gt; = {\n        &lt;std::_Head_base&lt;1ul, std::default_delete&lt;blob&gt;, true&gt;&gt; = {\n            &lt;std::default_delete&lt;blob&gt;&gt; = { },\n        },\n    },\n\n    &lt;std::_Head_base&lt;0ul, blob*, false&gt;&gt; = { _M_head_impl = 0x614c20 },\n};\n<\/pre>\n<p>Examining <tt>custom_blob<\/tt> looks like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">&lt;std::_Tuple_impl&lt;0ul, blob*, delete_blob&gt;&gt; = {\n\n    &lt;std::_Tuple_impl&lt;1ul, delete_blob&gt;&gt; = {\n        &lt;std::_Head_base&lt;1ul, delete_blob, false&gt;&gt; = {\n            _M_head_impl = { null_it = false }\n        },\n    },\n\n    &lt;std::_Head_base&lt;0ul, blob*, false&gt;&gt; = { _M_head_impl = 0x614c40 },\n}\n<\/pre>\n<p>Pay close attention to the <tt>_Head_base<\/tt>\u2019s third parameter, the boolean value we know as <tt>_IsEmptyNotFinal<\/tt>.  The only field that <tt>std::tuple<\/tt> places into memory is <tt>_M_head_impl<\/tt>, and we can see that a value of <tt>true<\/tt> elides the creation of this data in memory.<\/p>\n<p>It is also worth noticing that <tt>tuple<\/tt> reverses the in-memory ordering of its data.  This is because the implementation class defines the \u201ctail\u201d of the data-structure as its base class, stashing the \u201chead\u201d into the derived class.  As you likely already know, the layout of a class is such that the base classes exist first, and derived classes append any new fields past the end of the base class.  For our <tt>unique_ptr<\/tt> use-case, this means that we need to be a bit careful:<\/p>\n<ul>\n<li>Sometimes memory ordering is important for performance reasons, so an empty deleter means that we must offset the memory from the beginning of our <tt>unique_ptr<\/tt> by that much more every time we access the contained pointer.<\/li>\n<li>If we do something really nasty that assumes that the pointer exists at offset 0 of the <tt>unique_ptr<\/tt>, this assumption will be woefully incorrect in the custom deleter case.<\/li>\n<\/ul>\n<h1>Conclusions<\/h1>\n<p>This examination has lead me to a number of interesting conclusions here.<\/p>\n<p>It\u2019s obvious that we want the <tt>std::unique_ptr<\/tt> class to mimic the resource consumption and emitted code of a bald pointer as closely as possible, while adding useful functionality like automatic freeing of resources and exception safety.  Now we know that GCC actually accomplishes this for us under most circumstances.<\/p>\n<p>The vast majority of C++ applications will use the default deleter, but for those that require custom deleters, keep the following in mind:<\/p>\n<ul>\n<li>If at all possible, your custom deleter should be an \u201cempty\u201d class.  In other words, provide a simple functor with no member data.  This keeps <tt>std::unique_ptr<\/tt> memory-optimized.<\/li>\n<li>Do not mark your deleter as a <tt>final<\/tt> class.  At least on GCC, this disables the optimization because of the design of <tt>std::tuple<\/tt>.<\/li>\n<\/ul>\n<p>With respect to design principles, <tt>std::unique_ptr<\/tt> offers an interesting case-study in code that pushes certain design decisions into lower layers.  Since the container type, <tt>std::tuple<\/tt> knows how to take any empty class, and optimize it to consume zero bytes in memory, this optimization can be carried to more than just <tt>unique_ptr<\/tt>.  This results in less complicated code than what I had written using very targeted template specialization in <tt>unique_handle<\/tt>.<\/p>\n<p>Lastly, we learned that <tt>std::tuple<\/tt> occupies memory in the reverse order of its contained types, and by extension, a <tt>std::unique_ptr<\/tt> with a stateful deleter cannot be type-punned to its pointer type, as this would corrupt the deleter\u2019s internal state.<\/p>\n<hr>\n<p>Enjoy a <a href=\"\/download\/An_Exploration_of_unique_ptr.pdf\">printable version here<\/a>!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>(\u2026or \u201cHow I Learned to Love the Tuple.\u201d) Although the C++ standard does not explicitly spell it out, one may hope for a sophisticated implementation of the std::unique_ptr that consumes exactly as much memory as the pointer it tracks when it is configured to use a empty-class deleter (such as the default deleter type). The &hellip; <a href=\"https:\/\/cyberbisson.com\/blog\/2017\/04\/07\/an-exploration-of-the-deleter-on-the-memory-footprint-of-stdunique_ptr\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;An Exploration of the Deleter on the Memory Footprint of <tt>std::unique_ptr<\/tt>&#8220;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[35],"tags":[36,37,38,39,12,40,41],"class_list":["post-64","post","type-post","status-publish","format-standard","hentry","category-c","tag-c","tag-deleter","tag-gcc","tag-programming","tag-software","tag-tuple","tag-unique_ptr"],"_links":{"self":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts\/64","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/comments?post=64"}],"version-history":[{"count":10,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts\/64\/revisions"}],"predecessor-version":[{"id":115,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/posts\/64\/revisions\/115"}],"wp:attachment":[{"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/media?parent=64"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/categories?post=64"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cyberbisson.com\/blog\/wp-json\/wp\/v2\/tags?post=64"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}