前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >STL 设计之 EBO(空基类优化)

STL 设计之 EBO(空基类优化)

作者头像
公众号guangcity
发布于 2019-10-24 12:37:21
发布于 2019-10-24 12:37:21
2.1K00
代码可运行
举报
文章被收录于专栏:光城(guangcity)光城(guangcity)
运行总次数:0
代码可运行

STL 设计之 EBO(空基类优化)

0.导语

EBO 简称 Empty Base Optimization。

本节从空类开始,到 STL 内部,到测试,再到我们自己实现一个 EBO,对比性能,最后再测试,总结。

1.空类

定义一个空类:没有成员变量,没有继承,没有数据元素的类。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Empty{
public:
    void print() {
        std::cout<<"I am Empty class"<<std::endl;
    }
};

由于它是空的,所以这个 sizeof 是多少呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::cout<<sizeof(Empty)<<std::endl; //1

结果是 1,它是空的怎么不是 0 呢?

因为空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以上述大小为 1.

根据上面的回答,估计大家或引出另一个问题:为什么两个不同对象的地址应该不同?

现在有下面这个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Empty{
public:
    void print() {
        std::cout<<"I am Empty class"<<std::endl;
    }
};
template < typename T >
bool isSame( T const & t1, T const & t2 )
{
    return &t1 == &t2;
}

我们来测试一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main() {
    Empty a,b;
    assert(!isSame(a,b));  // 编译通过,a与b的地址不同

    Empty *p=new Empty;
    Empty *q=new Empty;
    assert(!isSame(p,q)); // 编译通过,a与b的地址不同
    return 0;
}

上面测试了,两个不同对象地址是不同的,考虑下面场景:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Empty a,b;
// 在同一地址具有两个对象将意味着在使用指针引用它们时将无法区分这两个对象。
Empty *p1=&a;
p1->print();

此时会发现,如果 a 的地址与 b 的地址一样,那么在同一地址具有两个对象将意味着在使用指针引用它们时将无法区分这两个对象。因此两个不同对象的地址不同。

2.空基类优化

现在对比一下下面两个用法,第一种,一个类中包含了两一个类作为成员,然后通过这个来获得被包含类的功能。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class notEbo  {
    int i;
    Empty e;
    // do other things
};

另一种直接采用继承的方式来获得基类的成员函数及其他功能等等。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class ebo:public Empty {
    int i;
    // do other things
};

接下来做个测试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::cout<<sizeof(notEbo)<<std::endl;
std::cout<<sizeof(ebo)<<std::endl;

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
8
4

第一种,会因为字节对齐,将其原来只占 1 字节,进行扩充到 4 的倍数,最后就是 8 字节。

对比这两个发现,第二种通过继承方式来获得基类的功能,并没有产生额外大小的优化称之为 EBO(空基类优化)。

接下来,我们回到 STL 源码中,看看其中的使用!

3.STL 中的 EBO 世界

不管是 deque、rb_tree、list 等容器,都离不开内存管理,在这几个容器中都包含了相应的内存管理,并通过_xx_impl来继承下面这几个类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::__mt_alloc<_Tp>
__gnu_cxx::__pool_alloc<_Tp>
__gnu_cxx::malloc_allocator<_Tp>

那这和我们的 EBO 有啥关系呢?

实际上,上面所列出继承的基类都是内存管理的 EBO(空基类)。

在每个容器中的使用都是调用每个内存管理的rebind<_Tp>::other

例如红黑树源码结构:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
        rebind<_Rb_tree_node<_Val> >::other _Node_allocator;
struct _Rb_tree_impl : public _Node_allocator
{
// do somethings
};

接下来我们看上面列出的内存管理类里面的源码结构:这里拿allocator举例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
	 template<typename _Tp1>
     struct rebind { typedef allocator<_Tp1> other; };
};

看到了没,通过rebind<_Tp>::other来获得传递进来的内存分配器,也就是前面提到的这些。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::bitmap_allocator<_Tp>
__gnu_cxx::__mt_alloc<_Tp>
__gnu_cxx::__pool_alloc<_Tp>
__gnu_cxx::malloc_allocator<_Tp>

搞懂了这些,来测试一波:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void print() {
    cout<<sizeof(std::allocator<int>)<<" "<<sizeof(std::allocator<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::bitmap_allocator<int>)<<" "<<sizeof(__gnu_cxx::bitmap_allocator<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::new_allocator<int>)<<" "<<sizeof(__gnu_cxx::new_allocator<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::__mt_alloc<int>)<<" "<<sizeof(__gnu_cxx::__mt_alloc<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::__pool_alloc<int>)<<" "<<sizeof(__gnu_cxx::__pool_alloc<int>::rebind<int>::other)<<endl;
    cout<<sizeof(__gnu_cxx::malloc_allocator<int>)<<" "<<sizeof(__gnu_cxx::malloc_allocator<int>::rebind<int>::other)<<endl;
}

我们来测试这些 sizeof 是不是 1,经过测试输出如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1 1
1 1
1 1
1 1
1 1
1 1

说明内存管理的实现就是通过采用继承的方式,使用空基类优化,来达到尽量降低容器所占的大小。

4.利用 EBO,手动实现一个简单的内存分配与释放

首先定义一个 sizeof(class)=1 的类,同 STL 一样,里面使用 allocate 与 deallocate 来进行内存管理。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class MyAllocator {
public:
    void *allocate(std::size_t size) {
        return std::malloc(size);
    }

    void deallocate(void *ptr) {
        std::free(ptr);
    }
};

第一种方式的内存管理:嵌入一个内存管理类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class T, class Allocator>
class MyContainerNotEBO {
    T *data_ = nullptr;
    std::size_t capacity_;
    Allocator allocator_;   // 嵌入一个MyAllocator
public:
    MyContainerNotEBO(std::size_t capacity)
            : capacity_(capacity), allocator_(), data_(nullptr) {
        std::cout << "alloc malloc" << std::endl;
        data_ = reinterpret_cast<T *>(allocator_.allocate(capacity * sizeof(T))); // 分配内存
    }

    ~MyContainerNotEBO() {
        std::cout << "MyContainerNotEBO free malloc" << std::endl;
        allocator_.deallocate(data_);
    }
};

第二种方式:采用空基类优化,继承来获得内存管理功能

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class T, class Allocator>
class MyContainerEBO
        : public Allocator {    // 继承一个EBO
    T *data_ = nullptr;
    std::size_t capacity_;
public:
    MyContainerEBO(std::size_t capacity)
            : capacity_(capacity), data_(nullptr) {
        std::cout << "alloc malloc" << std::endl;
        data_ = reinterpret_cast<T *>(this->allocate(capacity * sizeof(T)));
    }

    ~MyContainerEBO() {
        std::cout << "MyContainerEBO free malloc" << std::endl;
        this->deallocate(data_);
    }
};

开始测试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main() {
    MyContainerNotEBO<int, MyAllocator> notEbo = MyContainerNotEBO<int, MyAllocator>(0);
    std::cout << "Using Not EBO Test sizeof is " << sizeof(notEbo) << std::endl;
    MyContainerEBO<int, MyAllocator> ebo = MyContainerEBO<int, MyAllocator>(0);
    std::cout << "Using EBO Test sizeof is " << sizeof(ebo) << std::endl;

    return 0;
}

测试结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc

我们发现采用 EBO 的设计确实比嵌入设计好很多。至此,本节学习完毕。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 光城 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
RxJava系列番外篇:一个RxJava解决复杂业务逻辑的案例
之前写过一系列RxJava1的文章,也承诺过会尽快有RxJava2的介绍。无奈实际项目中还未真正的使用RxJava2,不敢妄动笔墨。所以这次还是给大家分享一个使用RxJava1解决问题的案例,希望对大家在使用RxJava的时候有一点点启发。对RxJava还不了解的同学可以先去看看我之前的RxJava系列文章: RxJava系列1(简介) RxJava系列2(基本概念及使用介绍) RxJava系列3(转换操作符) RxJava系列4(过滤操作符) RxJava系列5(组合操作符) RxJava系列6(从
张磊BARON
2018/04/13
1.4K0
RxJava系列番外篇:一个RxJava解决复杂业务逻辑的案例
RxJava(三) flatMap 操作符用法详解
还是以上一篇map操作符的例子吧,如果对 map操作符 不是很了解的,可以看看我之前的文章。
全栈程序员站长
2022/09/05
1.8K0
RxJava从入门到不离不弃(一)——基本概念和使用
RxJava的编程思想已经在Android开发者中变得越来越流行。有个不好的点就是上手不太容易,尤其是大部分人之前都是使用命令式编程语言。
蜻蜓队长
2018/08/03
7920
RxJava从入门到不离不弃(一)——基本概念和使用
【译】对RxJava中-repeatWhen()和-retryWhen()操作符的思考
第一次见到.repeatWhen()和.retryWhen()这两个操作符的时候就非常困惑了。不得不说,它们绝对是“最令人困惑弹珠图”的有力角逐者。
用户1740424
2018/07/23
2.1K0
【译】对RxJava中-repeatWhen()和-retryWhen()操作符的思考
【译】使用RxJava代替EventBus类库
如今的Android社区,人人都在讨论RxJava以及为什么我们应该在项目中使用RxJava。当我们开始在Android项目中使用RxJava的时候,就已经意识到了,我们的代码库可以不再需要Otto了(或其他事件总线类库)。
用户1740424
2018/07/23
4430
【译】使用RxJava代替EventBus类库
【译】RxJava变换操作符:-concatMap(-)与-flatMap(-)的比较
是时候回归写作了。(译者注:原作者吧啦吧啦唠家常,这里就不做翻译了,但是,有两个重要的链接,点我,再点我)
用户1740424
2018/07/23
8240
【译】RxJava变换操作符:-concatMap(-)与-flatMap(-)的比较
RxAndroid从零开始学之五(常见操作符与三级缓存)
RxAndroid的操作符有很多,本以为写了上一节的一些基本的Operator就可以正常编写代码了,但是后来在github上看googlesample,发现了一些另外的Operator。那么本文就继续介绍这些operator并加上自己的一些理解。
Frank909
2019/01/14
6350
【译】使用RxJava实现延迟订阅
我越来越喜欢把RxJava的defer()操作符作为一个工具来使用,以确保Observable代码在被订阅后才执行(而不是创建后立即执行)。我之前写过一些有关defer()的代码,但是,现在我想做更详细的描述。
用户1740424
2018/07/23
8390
【译】使用RxJava实现延迟订阅
【译】避免打断链式结构:使用.compose( )操作符
*[Chains break by the weakest link](https://www.flickr.com/photos/hernanpc/7115374283)*
小鄧子
2018/08/20
6610
RxJava简析
rxjava文档地址https://mcxiaoke.gitbooks.io/rxdocs/content/ 这个是中文版的
用户3112896
2020/11/25
7350
Android RxJava 实战系列:从磁盘 / 内存缓存中 获取缓存数据
Carson_Ho的Github地址 = RxJava2实战系列:从磁盘 / 内存缓存中 获取缓存数据
Carson.Ho
2019/02/22
2K0
Android应用架构
来源: 小鄧子(@Rx小鄧子) Android开发生态圈的节奏非常之快。每周都会有新的工具诞生,类库的更新,博客的发表以及技术探讨。如果你外出度假一个月,当你回来的时候可能已经发布了新版本的Support Library或者Play Services 我与Ribot Team一起做Android应用已经超过三年了。这段时间,我们所构建的Android应用架构和技术也在不断地演变。本文将向您阐述我们的经验,错误以及架构变化背后的原因。 曾经的架构 追溯到2012年我们的代码库使用的是基本结构,那个时候我们没有
编程范 源代码公司
2018/04/16
1.3K0
Android应用架构
【译】RxJava中的事件广播
如果你想多点传播一个事件,也就是向所有的下游操作符或订阅者发送同一个事件。这在做耗时操作如网络请求等场景来讲是非常有用的。你不需要为每个订阅者做重复的网络请求,只需执行一次,然后传播响应结果即可。
用户1740424
2018/07/23
8270
【译】RxJava中的事件广播
RxJava三问—基础知识点回顾
然后开始提问题了,Rxjava涉及的内容很多,我还是会以三个问题为单位,从易到难,一篇篇的说下去,今天的三问是:
码上积木
2020/12/11
6280
Android RxJava应用:合并数据源
Rxjava由于其基于事件流的链式调用、逻辑简洁 & 使用简单的特点,深受各大 Android开发者的欢迎。
Carson.Ho
2022/03/25
7940
Android RxJava应用:合并数据源
大佬们,一波RxJava 3.0来袭,请做好准备~
每个Android开发者,都是爱RxJava的,简洁线程切换和多网络请求合并,再配合Retrofit,简直是APP开发的福音。不知不觉,RxJava一路走来,已经更新到第三大版本了。不像RxJava 2对RxJava 1那么残忍,RxJava 3对RxJava 2的兼容性还是挺好的,目前并没有做出很大的更改。RxJava2到2020年12月31号不再提供支持,错误时同时在2.x和3.x修复,但新功能只会在3.x上添加。
Rouse
2019/07/17
1.9K0
大佬们,一波RxJava 3.0来袭,请做好准备~
一篇博客让你了解RxJava
RxJava可以说是2016年最流行的项目之一了,最近也接触了一下RxJava,于是想写一篇博客,希望能通过这篇博客让大家能对其进行了解,本篇博客是基于RxJava2.0,跟RxJava1.0还是有很多不同的
老马的编程之旅
2022/06/22
5440
一篇博客让你了解RxJava
【译】RxJava中的事件广播
如果你想多点传播一个事件,也就是向所有的下游操作符或订阅者发送同一个事件。这在做耗时操作如网络请求等场景来讲是非常有用的。你不需要为每个订阅者做重复的网络请求,只需执行一次,然后传播响应结果即可。
小鄧子
2018/08/20
5980
【译】RxJava中的事件广播
Android RxJava 实战讲解:合并数据源 & 同时展示数据
前言 Rxjava,由于其基于事件流的链式调用、逻辑简洁 & 使用简单的特点,深受各大 Android开发者的欢迎。 如果还不了解RxJava,请看文章:Android:这是一篇 清晰 & 易懂的R
Carson.Ho
2019/02/22
3.6K2
RxJava 2.0还没熟悉,RxJava 3.0说来就来了!(多种操作符代码详解篇)
在上篇文章中讲的是关于Rxjava的基础篇,今天来讲讲多种操作符的具体内容,操作符太多了,大家准备好啊,耐心看~
Android技术干货分享
2019/07/19
2.2K0
RxJava 2.0还没熟悉,RxJava 3.0说来就来了!(多种操作符代码详解篇)
推荐阅读
相关推荐
RxJava系列番外篇:一个RxJava解决复杂业务逻辑的案例
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档