You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
small_vector<int,2> vec;
vec.push_back(0); // Stored in-place on stack
vec.push_back(1); // Still on the stack
vec.push_back(2); // Switches to heap buffer.
// Lifecycle observers.//// Use small_vector to avoid heap allocation for up to two observers, unless// mobile, in which case we fallback to std::vector to prioritize code size.using LifecycleObserverVecImpl = conditional_t<
!kIsMobile,
folly::small_vector<AsyncTransport::LifecycleObserver*, 2>,
std::vector<AsyncTransport::LifecycleObserver*>>;
LifecycleObserverVecImpl lifecycleObservers_;
// push_backvoidAsyncSocket::addLifecycleObserver(
AsyncTransport::LifecycleObserver* observer) {
lifecycleObservers_.push_back(observer);
}
// for loopfor (constauto& cb : lifecycleObservers_) {
cb->connect(this);
}
// iterator / eraseconstauto eraseIt = std::remove(
lifecycleObservers_.begin(), lifecycleObservers_.end(), observer);
if (eraseIt == lifecycleObservers_.end()) {
returnfalse;
}
<Any integral type> : 指定small_vector中size和capacity的数据类型。
// With space for 32 in situ unique pointers, and only using a 4-byte size_type.
small_vector<std::unique_ptr<int>, 32, uint32_t> v;
// A inline vector of up to 256 ints which will not use the heap.
small_vector<int, 256, NoHeap> v;
// Same as the above, but making the size_type smaller too.
small_vector<int, 256, NoHeap, uint16_t> v;
// This value should we multiple of word size.staticsize_tconstexprkHeapifyCapacitySize = sizeof(typename std::aligned_storage<sizeof(InternalSizeType), alignof(value_type)>::type);
// Threshold to control capacity heapifying.staticsize_tconstexprkHeapifyCapacityThreshold = 100 * kHeapifyCapacitySize;
/* * Figure out the max number of elements we should inline. (If * the user asks for less inlined elements than we can fit unioned * into our value_type*, we will inline more than they asked.)*/staticconstexpr std::size_t MaxInline{
constexpr_max(sizeof(Value*) / sizeof(Value), RequestedMaxInline)};
即通过malloc_usable_size获取capacity。AllocationSize{}(u.pdata_.heap_)/sizeof(value_type);直接理解为malloc_usable_size(u.pdata_.heap_)/sizeof(value_type)即可,加AllocationSize是为了解决一个Android not having malloc_usable_size below API17,不是重点。
// 优化前voidclear() {
erase(begin(), end());
}
// 优化后voidclear() {
// Equivalent to erase(begin(), end()), but neither Clang or GCC are able to optimize away the abstraction.for (auto it = begin(); it != end(); ++it) {
it->~value_type();
}
this->setSize(0);
}
namespacempl= boost::mpl;
template <
classValue,
std::size_t RequestedMaxInline,
classInPolicyA,
classInPolicyB,
classInPolicyC>
structsmall_vector_base {
typedef mpl::vector<InPolicyA, InPolicyB, InPolicyC> PolicyList;
/* * Determine the size type*/typedeftypename mpl::filter_view<
PolicyList,
std::is_integral<mpl::placeholders::_1>>::type Integrals;
typedeftypename mpl::eval_if<
mpl::empty<Integrals>,
mpl::identity<std::size_t>,
mpl::front<Integrals>>::type SizeType;
/* * Determine whether we should allow spilling to the heap or not.*/typedeftypename mpl::count<PolicyList, small_vector_policy::NoHeap>::type
HasNoHeap;
/* * Make the real policy base classes.*/typedef IntegralSizePolicy<SizeType, !HasNoHeap::value> ActualSizePolicy;
/* * Now inherit from them all. This is done in such a convoluted * way to make sure we get the empty base optimizaton on all these * types to keep sizeof(small_vector<>) minimal.*/typedef boost::totally_ordered1<
small_vector<Value, RequestedMaxInline, InPolicyA, InPolicyB, InPolicyC>,
ActualSizePolicy>
type;
};
介绍
行为与std::vector类似,但是使用了small buffer optimization(类似于fbstring中的SSO),将指定个数的数据内联在对象中,而不是像std::vector直接将对象分配在堆上,避免了malloc/free的开销。
small_vector基本兼容std::vector的接口。
small_vector<int,2> vec
指定可以内联在对象中2个数据:当超过2个后,后续添加的数据会被分配到堆上,之前的2个数据也会被一起move到堆上:
使用场景
根据官方文档的介绍,small_vector在下面3种场景中很有用:
allocation capacity
(这会显著的降低insertion/reallocation效率,如果对这两个操作的效率比较在意,你应该使用FBVector,FBVector在官方描述中可以完全代替std::vector)比如在io/async/AsyncSocket.h中,根据条件的不同使用small_vector或者std::vector:
为什么不是std::array
下面两种情况,small_vector比std::array更合适:
其他用法
NoHeap
: 当vector中数据个数超过指定个数时,不会再使用堆。如果个数超过指定个数,会抛出std::length_error
异常。<Any integral type>
: 指定small_vector中size和capacity的数据类型。其中,依赖boost::mpl元编程库,可以让后两个模板变量任意排序。
其他类似库
Benchmark
没有找到官方的benchmark,自己简单的写了一个,不测试数据溢出到堆上的情况。
插入4个int,std::vector使用reserve(4)预留空间。
结果是:small_vector比std::vector快了40%:
如果把stdVector中的
vec.reserve(4);
去掉,那么small_vector速度比std::vector快了3倍。在我的环境上,std::vector的扩容因子为2,如果不加reserve,那么std::vector会有2次扩容的过程(貌似很多人不习惯加reserve,是有什么特别的原因吗 : )):代码关注点
small_vector代码比较少,大概1300多行。主要关注以下几个方面:
主要类
union Data
、struct HeapPtr
、struct HeapPtrWithCapacity
,这三个字段负责数据的存储。此外small_vector对外暴露API接口,例如push_back、reserve、resize等。small_vector
声明:
声明中有三个策略模板参数是因为在一次提交中删除了一个无用的策略,OneBitMutex:Delete small_vector's OneBitMutex policy。
small_vector_base
boost::mpl放到最后说吧 :)
数据结构
small_vector花了一些心思在capacity的设计上,尽可能减小对象内存,降低内联数据带来的影响。
union Data
负责存储数据:InlineStorageType
使用std::aligned_storage进行初始化,占用空间是
sizeof(value_type) * MaxInline
,对齐要求为alignof(value_type):capacity
与std::vector用结构体字段表示capacity不同,small_vector的capacity存放分为三种情况。
capacity内联在对象中
这是最简单的一种情况:
条件为
sizeof(HeapPtrWithCapacity) < sizeof(InlineStorageType)
(这里我不明白为什么等于的情况不算在内):通过malloc_usable_size获取capacity
假如上述
kHasInlineCapacity == false
,即sizeof(InlineStorageType) <= sizeof(HeapPtrWithCapacity)
时,考虑到节省对象空间,capacity不会内联在对象中,此时PointerType的类型为HeapPtr,内部只保留一个指针:那么此时capacity存放在哪里了呢?这里又分了两种情况,第一种就是这里要说明的:直接通过malloc_usable_size获取从堆上分配的内存区域的可用数据大小,这个结果就被当做small_vector当前的capacity:
但是有一个问题,由于内存分配存在alignment和minimum size constraints的情况,malloc_usable_size返回的大小可能会大于申请时指定的大小,但是folly会利用这部分多余的空间来存放数据(如果能放的下)。
比如在不使用jemalloc的情况下,在扩容的函数内,将向系统申请的字节数、malloc_usable_size返回的可用空间、small_vector的capacity打印出来:
可以看出,扩容时即使向系统申请16字节的空间,malloc_usable_size返回的是24字节,而small_vector此时的capacity也是24,即会利用多余的8个字节额外写入2个数据。
**如果使用了jemalloc **,那么会根据size classes分配空间。
这种方式也是有使用条件的,即
needbytes >= kHeapifyCapacityThreshold
,kHeapifyCapacityThreshold的定义为:我没想明白这个100是怎么定下来的 :(
将capacity放到堆上
当需要申请的内存
needbytes >= kHeapifyCapacityThreshold
时,就会直接将capacity放到堆上进行管理:此时需要多申请sizeof(InternalSizeType)字节来存放capacity,并且需要对内存分配接口返回的指针加上sizeof(InternalSizeType)从而指向真正的数据:
size()相关
那么如何区分数据是内联在对象中还是溢出到堆上,如何区分上面三种capacity存储策略呢?
采取的做法是向size借用两位来区分:
都是很简单的位运算。只需要注意下policyMaxSize函数,因为向size借了2位,所以最大的size不是SizeType类型的最大值,需要有额外的判断。
capacity()函数
因为capacity有三种存储方式,所以需要根据各自情况去获取:
数据内联在对象中
这是最简单的情况,根据上面说过的isExtern()判断数据是否内联,是的话直接返回MaxInline。
这里的MaxInline还不是上游传过来的RequestedMaxInline。因为不管是什么capacity存储策略,union Data必然会有一个有指针,最小也是
sizeof(void*)
,假如用户传的是small_vector<uint8_t,1>,会替用户修改MaxInine = 8,最大限度利用对象空间:为capacity分配了内存
这里包括capacity分配的内存在堆上或者内联在对象中。通过hasCapacity()判断,isHeapifiedCapacity上面说过:
如果hasCapacity()为true,则调用u.getCapacity(),可以猜到这个方法调用PointerType(HeapPtr/HeapPtrWithCapacity)对应的getCapacity()方法。
注意unshiftPointer是shiftPointer的反过程,将指针从指向真正的数据回退到指向capacity。
不为capacity分配空间
即通过malloc_usable_size获取capacity。
AllocationSize{}(u.pdata_.heap_)/sizeof(value_type);
直接理解为malloc_usable_size(u.pdata_.heap_)/sizeof(value_type)
即可,加AllocationSize是为了解决一个Android not having malloc_usable_size below API17,不是重点。扩容
与std::vector扩容的不同点:
noexcept move constructor
来决定调用move constructor
还是copy constructor
(之前这篇文章提到过:c++ 从vector扩容看noexcept应用场景)。但small_vector没有这个过程,有move constructor
就直接调用,不判断是否有noexcept。所以,当调用move constructor
有异常时,原有内存区域的数据会被破坏最终扩容流程都会走到moveToUninitialized函数。
这里我不太理解的一点是,既然注释中提到假设前后内存没有overlap,那么在is_trivially_copyable的版本中为什么不用std::memcpy呢?效率还能高一点。
先迁移原有数据还是先放入新数据
在之前的版本中,流程是申请新内存、迁移原有数据、放入新数据:
在small_vector improvements提交中,改为了申请新内存、放入新数据、迁移原有数据。理由是有可能emplace_back的数据是small_vector中的某一个数据的引用, 比如这次提交中加的ForwardingEmplaceInsideVector test,不过这属于corner case。
makeGuard
在Support -fno-exceptions in folly/small_vector.h提交里,使用makeGuard代替了原有的try-catch。makeGuard定义在folly/ScopeGuard.h中。
之前try-catch版本:
可以对比上面的makeGuard版本,逻辑没有变化。
Andrei Alexandrescu和Petru Marginean在2000年写的Generic: Change the Way You Write Exception-Safe Code — Forever中,介绍了编写Exception-Safe Code常见的方法并提出了ScopeGuard的概念:
其他
clear优化
在optimize small_vector::clear提交中,利用了Clang/GCC对clear函数进行了优化,生成更少的汇编指令(这都是怎么发现的???):
__attribute__((__pack__))/pragma(pack(push, x))
folly中包装了编译器对齐系数,相关参数介绍 : C/C++内存对齐详解
在Fix alignment issues in small_vector提交中,small_vector改为了只在64位平台使用对齐系数(不知道为什么,哭。。。):
boost::mpl元编程库
boost mpl文档
small_vector_base的代码很少,关注下boost::mpl的使用:
解析模板参数的思路为:
typedef mpl::vector<InPolicyA, InPolicyB, InPolicyC> PolicyList;
: 将策略放入到mpl::vector中,例如Noheap、指定size的数据类型(uint32_t、uint16_t等)接下来可以分为两块,获取SizeType和获取HasNoHeap:
获取SizeType:
typedef typename mpl::filter_view<PolicyList, std::is_integral<mpl::placeholders::_1>>::type Integrals;
: 将符合std::is_integral
条件的筛选出来,比如uint8_t。mpl::filter_view
被定义为template< typename Sequence, typename Pred>
,其中,Pred需要是Unary Lambda Expression
,即为编译期可调用的entity,分为Metafunction Class
和Placeholder Expression
,上面的std::is_integral<mpl::placeholders::_1>
即为Placeholder Expression
。typedef typename mpl::eval_if<mpl::empty<Integrals>, mpl::identity<std::size_t>, mpl::front<Integrals>>::type SizeType;
不用知道每个函数的意思,从字面也能看出来:假如应用层传入的模板参数没有std::is_integral,那么SizeType,即size的类型,就是size_t,否则就是应用传入的类型,比如uint8_t.获取HasNoHeap:
typedef typename mpl::count<PolicyList, small_vector_policy::NoHeap>::type HasNoHeap;
,这个也能猜出来:应用层是否指定了NoHeap.可以看出,解析模板参数并没有依赖参数的特定顺序。
boost/operators
boost/operators文档参考 : Header <boost/operators.hpp>
small_vector_base的最后两行代码,与boost/operators有关 :
boost/operators为精简operator代码而设计,比如我们想要支持x < y,那么x > y、x >= y、和x <= y同样也需要。但是理想情况下,我们只需要重载
operator<
就行了,后面三个操作可以根据x<y推导出来,boost/operators可以为我们生成类似下面的代码:boost::totally_ordered1被small_vector继承。按照boost::totally_ordered1的定义,只要实现
operator<
和operator=
,那么除了这两个操作,boost/operators也会自动生成operator>
、operator<=
、operator>=
、operator!=
对比std::vector,在c++20之前,std::vector实现了所有的
operator==
、operator!=
、operator<
、operator<=
、operator>
、operator>=
,比较繁琐。c++20的<=> (three-way comparison)
c++20提供了一个新特性,three-way comparison,可以提供上面boost/operators的功能。可以参考:
比如,std::vector在c++20后,就废弃了
<、<=、 >=、!= operators
(下面引用来自于operator==,!=,<,<=,>,>=,<=>(std::vector)):
(完)
朋友们可以关注下我的公众号,获得最及时的更新:
The text was updated successfully, but these errors were encountered: