什么是Empty Base Optimization?
說到C++中的Empty Base Optimization(簡稱ebo)可能大家還是比較陌生,但是C++中每天都在用的std::string
中就用到了ebo。
那么到底什么是ebo呢?其實ebo就是當一個類的對象理想內存占用可以為0的時候,把這個類的對象作為另一個類的成員時,把其內存占用變為0的一種優化方法。說起來可能有點繞,還是用一個例子來說明一下吧,看下面的代碼:
#include
usingnamespacestd;
classBase
{};
intmain()
{
cout<"sizeof(Base)"<sizeof(Base)<"addrobj1"<(void*)&obj1<"addrobj2"<(void*)&obj2<return0;
}
大家能猜到上面的代碼的輸出嗎?sizeof(Base)
會是0嗎?obj1
的地址會和obj2
的一樣嗎?
自己編譯上面的代碼,運行一下,會得到類似下面的輸出(第2、3行會略有不同):
sizeof(Base)1
addrobj10xbfdc9033
addrobj20xbfdc9032
看見了吧?就算Base
不包含任何的成員,編譯器也會讓Base
占1 byte。這是因為如果一個類的內存占用為0,那么連續的分配對象有可能會有同一個內存地址,這個是不合理的。所以編譯器為了避免這種情況,讓空的類也會占有1 byte的大小。
那么如果我要用Base
作為另一個類的成員變量呢,比如下面這樣:
classTestCls
{
Basem_obj;
intm_num;
};
intmain()
{
cout<"sizeof(TestCls)"<sizeof(TestCls)<return0;
}
知道上面的輸出會是多少嗎?5?在32位的機器上面是8,因為編譯器為了存取的方便,會在m_obj
的后面產生3 byte的padding,以和機器字對齊。總之答案不會是4。
但是在內存非常緊張的情況下,還真的會想要讓TestCls
的size是4。有辦法嗎?這里就可以用到今天介紹的ebo
了,看下面的代碼:
classTestCls:publicBase
{
intm_num;
};
intmain()
{
cout<"sizeof(TestCls)"<sizeof(TestCls)<return0;
}
這次能猜到輸出是多少嗎?沒錯,就是我們想要的4!當我們把空的類作為基類的時候,編譯器就會把這個基類的size去掉,做了優化, 從而使得整個對象占有真正需要的size。
那么如果這個子類除了基類之外,沒有別的成員呢?如下面:
classTestCls:publicBase
{};
intmain()
{
cout<"sizeof(TestCls)"<sizeof(TestCls)<return0;
}
上面的代碼輸出仍然是1,因為如果這個類本身除了空基類之外沒別的成員, 說明這個類本身也是一個空類,所以最開始說的情況就適用于這里。編譯器就給空類給了1的size。
上面說的就是Empty Base Optimization了。那么現實中哪里使用到了這個技巧呢?除了最開始提到的std::string
之外,Google的cpp-btree也用到了這個技巧。下面我們來看看這兩個現實中的例子。
STL中的string
C++每天都用的string中就用到了ebo。我們來看看string是如何定義成員的(省略函數定義,以下代碼源自gcc 4.1.2 c++):
template<typename_CharT,typename_Traits,typename_Alloc>
classbasic_string
{
public:
mutable_Alloc_hider_M_dataplus;
};
注意string
實際上是模板類basic_string
的一個特化類。而basic_string
只包含了一個成員_M_dataplus
, 其類型為_Alloc_hider
。
我們來看看_Alloc_hider
是怎么定義:
template<typename_CharT,typename_Traits,typename_Alloc>
classbasic_string
{
private:
struct_Alloc_hider:_Alloc//Useebo
{
_CharT*_M_p;//Theactualdata.
};
};
_Alloc_hider
繼承于模板參數類_Alloc
(并且還是私有繼承),還有一個自己的成員_M_p
。_M_p
是用來存放實際數據的,而_Alloc
呢?熟悉STL的人可能還記得STL里面有一個allocator。這個allocator一般的實現都是沒有任何的數據成員,只有static函數的。所以這個類是一個空類。默認的string就是將這個allocator當作模板參數傳遞到_Alloc
。所以_Alloc
大多數情況下都是空類,而string經常會在程序中用到, 還很經常會大量的使用,比如在容器中,這個時候就需要考慮內存占用了。所以在這里就是用了ebo的優化。
可能會有人會問,string
里面實際上只有char*
,但是不是說string
還記錄了size, 還用到了copy on write技術的嗎?那怎么只有一個char*
呢?這個和string
的實現中的內存布局相關,其中Copy on write是g++的stl中實現的策略, 想要了解g++的string的內存布局,可以看看陳碩的這篇文章。
cpp-btree中的ebo
cpp-btree是Google出的一個基于B樹的模板容器類庫。如果有不熟悉B樹的童鞋,可以移步這里看一看這個數據結構的動畫演示。
B樹是一種平衡樹結構,一般常用于數據庫的磁盤文件數據結構(不過一般會用其變體B+樹)。而cpp-btree則是全內存的,和std::map
類似的一種容器實現,其對于大量元素(>100w)的存取效率要高于std::map
的紅黑樹實現,并且還節省內存。
關于cpp-btree的廣告就賣到這里,我們看看他哪里使用了ebo。在cpp-btree里面提供了btree_set
和btree_map
兩個容器類, 而他們的公共實現都在btree
這個類里面。btree
這個類實現了主要的B樹的功能,而其成員定義如下:
template<typenameParams>
classbtree:publicParams::key_compare{
private:
typedeftypenameParams::allocator_typeallocator_type;
typedeftypenameallocator_type::templaterebind<char>::other
internal_allocator_type;
template<typenameBase,typenameData>
structempty_base_handle:publicBase{
empty_base_handle(constBase&b,constData&d)
:Base(b),
data(d){
}
Datadata;
};
empty_base_handleroot_;
};
可以看見btree
這個類里面只包含了root_
這一個成員,其類型為empty_base_handle
。empty_base_handle
是一個繼承于Base的類,在這里,Base
特化成internal_allocator_type
。從名字可以看出internal_allocator_type
是一個allocator, 而在默認的btree_map
實現中,這個allocator就是std::allocator
。所以一般情況下,Base
也是一個空類。
這里btree
也利用了ebo節省了內存占用。
一個例外
在編譯器判斷是否做ebo的時候,有這么一個例外,就是雖然繼承于一個空類, 但是子類的第一個非static成員的類型也是這個空類或者是這個類的一個子類。在這種情況下,編譯器是不會做ebo的。
有點繞,我們看看下面的代碼就明白了:
#include
usingnamespacestd;
classBase
{};
classTestCls:publicBase
{
public:
Basem_obj;//<<<<
intm_num;
};
intmain()
{
cout<"sizeof(Base)"<sizeof(Base)<"sizeof(TestCls)"<sizeof(TestCls)<"addrobj"<(void*)&obj<"addrobj.m_obj"<(void*)&(obj.m_obj)<"addrobj.m_num"<(void*)&(obj.m_num)<return0;
}
運行一下上面的代碼,你會看到,TestCls
的size是8,并且obj
的地址和obj.m_obj
的地址并不一樣。這說明了ebo并沒有進行。
-
內存
+關注
關注
8文章
2999瀏覽量
73883 -
C++
+關注
關注
22文章
2104瀏覽量
73489 -
代碼
+關注
關注
30文章
4747瀏覽量
68349
原文標題:Empty Base Optimization
文章出處:【微信號:CPP開發者,微信公眾號:CPP開發者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論