针对C ++ Lambda进行小缓冲区优化实验

TL; DR

我们实现了SmallFun ,它是std::function的替代产品,它实现了固定大小的捕获优化 (一种小型缓冲区优化的形式)。 尽管SmallFun通用性不如std::function ,但在某些基准测试中, 速度要快3-5倍

您可以在GitHub上查看代码

针对C ++ Lambda进行小缓冲区优化实验
Pascal RichierUnsplash上的照片

背景

std::function是一种方便的方式来存储带闭包的lambda(也称为捕获),同时提供统一的接口。 如果您来自OOP领域,那么将它们理解为策略模式的概括可能会有所帮助。

std::function和lambdas之前,我们将创建一个手工制作的仿函数对象,如下所示:

该存储库比较std::function ,手工制作的FunctorSmallFun 我们发现SmallFun通用性稍逊于std::function

std :: function的错失良机

std::function使用PImpl模式来提供统一的接口,该接口可消除给定签名的所有函子。

例如,这两个实例fg具有相同的大小,尽管捕获不同:

这是因为std::function将捕获存储在heap上 这统一了所有实例的大小,但这也是优化的机会!

怎么样?

代替在堆上动态分配内存,我们可以将函数对象(包括其虚拟表)放置到堆栈上的预分配位置。

这就是我们实现SmallFun ,它的用法非常类似于std::function

基准测试

考试

为了测试我们分配和调用函子的速度,我们将所有实例保存在向量中并循环执行。 将结果保存到另一个向量中,以确保优化器不会优化我们正在测试的内容。

SmallFun实施细节

要实现SmallFun ,我们需要结合三种C ++模式: type-erasurePImplplace -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