针对C ++ Lambda进行小缓冲区优化实验
TL; DR
我们实现了SmallFun
,它是std::function
的替代产品,它实现了固定大小的捕获优化 (一种小型缓冲区优化的形式)。 尽管SmallFun
通用性不如std::function
,但在某些基准测试中, 速度要快3-5倍 。
您可以在GitHub上查看代码 。
背景
std::function
是一种方便的方式来存储带闭包的lambda(也称为捕获),同时提供统一的接口。 如果您来自OOP领域,那么将它们理解为策略模式的概括可能会有所帮助。
在std::function
和lambdas之前,我们将创建一个手工制作的仿函数对象,如下所示:
该存储库比较std::function
,手工制作的Functor
和SmallFun
。 我们发现SmallFun
通用性稍逊于std::function
。
std :: function的错失良机
std::function
使用PImpl模式来提供统一的接口,该接口可消除给定签名的所有函子。
例如,这两个实例f
和g
具有相同的大小,尽管捕获不同:
这是因为std::function
将捕获存储在heap上 。 这统一了所有实例的大小,但这也是优化的机会!
怎么样?
代替在堆上动态分配内存,我们可以将函数对象(包括其虚拟表)放置到堆栈上的预分配位置。
这就是我们实现SmallFun
,它的用法非常类似于std::function
:
基准测试
考试
为了测试我们分配和调用函子的速度,我们将所有实例保存在向量中并循环执行。 将结果保存到另一个向量中,以确保优化器不会优化我们正在测试的内容。
SmallFun实施细节
要实现SmallFun
,我们需要结合三种C ++模式: type-erasure , PImpl和place -new 。
类型擦除
类型擦除将许多实现统一到一个接口中。 在我们的例子中,每个lambda(或函子)都有一个自定义的调用运算符和析构函数。 我们需要针对API使用者将使用的任何类型自动生成一个实现。
这将是我们的公共接口:
对于具有给定签名的任何可调用类型:
现在我们可以通过以下方式使用它:
这非常麻烦并且容易出错。 下一步将是一个容器。
皮普
PImpl分离,隐藏,管理实际实现的生命周期,并公开有限的公共API。
一个简单的实现可能看起来像这样:
这或多或少是如何实现std::function
的。
那么我们如何删除堆分配呢?
新刊登位置
新的Placement分配给定地址的内存。 例如:
放在一起
现在,我们只需要进行少量更改即可删除堆分配:
您可能已经注意到,如果Model<...>
的大小大于SIZE
,则会发生不良情况! 一个断言只会在运行时捕获,直到很晚……幸运的是,可以使用enable_if_t
在编译时捕获它。
但是首先,复制构造函数呢?
复制构造函数
与std::function
的实现不同,我们不能仅复制或移动std::shared_ptr
。 我们也不能只按位复制内存,因为lambda可能管理由于副作用而只能被释放一次的资源。 因此,我们需要使模型能够针对给定的内存位置进行复制构造。
我们只需要添加:
进一步说明
- 如我们所见,我们可以在编译时验证Lambda是否适合我们的内存。 如果没有,我们可以提供对堆分配的回退。
-
SmallFun
更通用实现将采用通用分配器。 - 我们注意到不能仅通过按位复制内存来复制内存。 但是使用类型特征,我们可以检查是否
基础数据类型为POD,然后按位复制。
既然你在这里...
我们创建了Buckaroo ,以便更轻松地集成C ++库。 如果您想尝试一下,最好的起点是文档 。 您可以浏览Buckaroo.pm上的现有软件包,或在愿望清单上请求更多。
From: https://hackernoon.com/experimenting-with-small-buffer-optimization-for-c-lambdas-d5b703fb47e4