一、SBO类型
在前面的文章中(小对象优化)对SBO,SOO等等进行了初步的分析和说明,其实在实际应用中,SBO\SSO\SOO这种小对象的控制是非常麻烦的。之所以这样说,是因为,大对象的处理一般频次和数量都比较少,即使出现较多回收后也容易管理。但小对象就不同了,小到可能一两个字节,大了也不过百十个字节到K的量级(不同的库和语言及平台定义这个略有不同)。
这就出来一个问题,小对象应用是高频的,如果经常分配内存空间太小,除了产生大量的内存碎片,有时候可能对齐导致的内存浪费也是非常惊人的。而且学过内存管理的都应该明白,在堆上开辟内存成本和代价都很大,同样回收也如此。所以对一些小内存的内存对象管理,其实是更倾向于在栈上解决。
栈和堆的不同则搞开发的基本常识,这里不做介绍。小对象在栈上的管理,有一个明显的优势,就是所谓的“管杀不管埋”,用完以后就扔掉就可以了。而在实际的开发场景中,可能会遇到巨型内存,大内存,中型内存,小内存,微小内存,之所以写这么多是因为很多内存管理的库根据内存大小的不同定义出来的,只要明白了意义,都可以顺利的理解为什么定义这些内存类型了。
二、类型擦除中遇到SBO
在前面的多线程任务类中,任务对象中可能是一个很简单的问题,甚至就是一个转发,也可能是一个数据流控制分析,需要分配不少内存,也就是说各种情况都可能发生。这就对前面的任务类提出了一个要求,怎么处理SBO类型,或者是如何在任务擦除里处理SBO系列的数据对象。在前面类型擦除提到过用函数指针实现,下面先看一个简单的描述:
class Task
{
public:
Base *pTask_ = nullptr;
char tBuf_[1024] = {0};
public:
void Create()
{
if (/*small*/)
{/*pTask 直接创建在tBuf_上*/}
else
{/*按原有方式创建*/}
}
~Task()
{/*根据不同情况来释放内存*/}
};
如果看过前面的几篇文章包括GC方面的源码分析,就可以很清楚的明白上面的代码的意思。但是这又引出一个问题,在创建对象时,如果确定对象是一个小对象还是一个普通对象呢?这就需要一些辅助的方式:
//常量设置:假设小对象就是128
constexpr size_t small_size = 6;
constexpr size_t max_align = alignof(std::max_align_t);
//C++ 17中的常量
template <typename W>
inline constexpr bool is_small =
sizeof(W) <= small_size
&& alignof(W) <= max_align;
class BaseTask
{
public:
//BaseTask() = default;
virtual ~BaseTask() = default;
public:
virtual void DoWork() const = 0;
virtual void operator()()const = 0;
};
template<typename F, bool is_small>
class TaskImpl :public BaseTask
{
public:
//TaskImpl() = default;
template<typename T>
TaskImpl(T&& t) :func_(std::forward<T>(t)) {}
~TaskImpl() = default;
public:
void DoWork()const override
{
//WorkTask();
std::cout << "start task!" << std::endl;
}
void operator()()const override
{
func_();
}
public:
F func_;
};
//偏特化堆
template<typename F>
class TaskImpl<F,false> :public BaseTask
{
public:
//TaskImpl() = default;
template<typename T>
TaskImpl(T&& t)
:func_(std::forward<T>(t))
{}
~TaskImpl() = default;
public:
void DoWork()const override
{
//WorkTask();
std::cout << "start task!" << std::endl;
}
void operator()()const override
{
func_();
}
public:
F func_;
};
#include <concepts>
class TaskWrapper;
//template <typename F>
//using is_ok_wrapper =
//std::enable_if_t<!std::is_same_v< std::remove_cvref_t<F>, TaskWrapper >,int>;
template <typename F>
concept is_ok_wrapper = !std::is_same_v<std::remove_cvref_t<F>, TaskWrapper>;
class TaskWrapper
{
public:
//TaskWrapper() = default;
//template<typename F, is_ok_wrapper<F> = 0>
template<is_ok_wrapper F>
TaskWrapper(F&& f)
{
using type_decay = std::decay_t<F>;
using standType = TaskImpl<type_decay,is_small<type_decay>>;
//typedef TaskImpl<F> standType;
if (is_small<type_decay>)
{
pTask_ = new(Buffer_)standType(std::forward<type_decay>(f));
}
else
{
pTask_ = new standType(std::forward<type_decay>(f));
}
}
~TaskWrapper() = default;
public:
TaskWrapper(TaskWrapper&& other)noexcept
:pTask_(std::move(other.pTask_))
{}
TaskWrapper& operator = (TaskWrapper&& rhs)noexcept
{
pTask_ = std::move(rhs.pTask_);
return *this;
}
TaskWrapper(const TaskWrapper&) = delete;
TaskWrapper& operator=(const TaskWrapper&) = delete;
public:
void operator()()const
{
pTask_->operator()();
//pTask_->DoWork();
}
public:
//std::unique_ptr<BaseTask> pTask_ = nullptr;
BaseTask* pTask_ = nullptr;
alignas(max_align) unsigned char Buffer_[small_size] = { 0 };//栈管理
};
在代码中用small_size来确定小对象的大小,当然也可以根据实际情况来使用其它的方式。另外,在不同的平台和不同的场景下都要考虑的一个问题就是字节对齐的问题,所以定义了一个max_align,再使用c++17中提供的常量表达式来定义is_small对整个字节数的控制。
在实际的打包类中,只要判断相关传入的数据类型即可。同时,在定义标准的TaskImpl中,增加一个布尔型非类型模板参数,再偏特化这个False布尔类型模板,通过它来控制具体的模板类的生成(即正常使用栈生成,Fasle使用堆生成)。
这里面有两个问题,一个是判断语句太丑陋,本身已经能够通过模板自动判断类型;另外一个就是偏特化和特化的类没有本质不同。这就引出了在前面的类型擦除里提到去除继承后的手动写函数指针(去除掉vptr),然后把内存的处理迁移到TaskImpl内部也就是模板和偏特化分别实现,就可以自动控制不再使用if语句。
这一块儿比较麻烦,以后专门写一篇文章分析一下。
三、总结
对这一块的学习还有待更深入的分析和理解,不过总体上来看,类型擦除在实际应用中还是亮点可挖掘的。C++的stl库中std::function就引入了这种技术,有兴趣的可以深入的分析一下这个函数的源码,就可以更清楚的了解SBO和类型擦除的应用。
文章探讨了C++中处理小对象(SBO)的优化策略,包括在栈上管理小对象以避免内存碎片和提高效率。通过类型擦除技术,文章展示了如何在任务类中处理SBO,利用模板和偏特化在栈和堆之间动态选择对象存储位置。同时,提到了C++标准库中的std::function作为类型擦除的例子。
60

被折叠的 条评论
为什么被折叠?



