技术 | 高性能C++的内存局部性模式

今天分享的文章是一篇关于提高维护高性能C++代码库的人机工程系效能的程序。本文将给出这些实用程序之一——OutOfLine的概述。
 

让我们从一个有意思的例子开始。假设您有一个打开大量路径的系统,也许它们是文件,也许它们被命名为UNIX套接字,也许是管道。但是无论出于什么原因,您在启动时打开了许多文件描述符,然后对这些描述符进行了大量处理。最后当您完成后,你关闭描述符并断开了路径链接。

 

一个(简写的)初始设计可能如下所示: 

 

 

这是一个逻辑合理的好设计。您可以分配一个包含这些内容的大数组,对它们进行操作。当数组的生命周期结束时,它们会自己清理。
 

但性能如何呢?假设您经常使用fd,并且只有在清理对象时才使用路径。现在我们有一个40B大小的对象的数组,并且我们的关键路径只使用其中的4B个,那么这意味着,当您保持“略过”90%的开销时,您将会看到更多的缓存行丢失。
 

对此一个非常常见的解决方案是从结构数组切换到数组结构——这也就是使我们的表现胜出的方法。但这将使我们失去RAII。有没有办法两全其美?

 

一个最初的折衷办法可能是不存储32B的std::string,而是存储只有8B的std::unique_ptr。这将使你的物体尺寸从40B降到16B——将是一个巨大的胜利,但它不如并行阵列好。

 

OutOfLine是一个工具,你可以用它来保持RAII,并把你的无用成员完全移出你的对象而没有空间开销。你通过继承来使用OutOfLine,就像这样。它是一个CRTP基类,所以第一个模板参数应该是继承的子类,第二个参数是应该与每个“快速”对象相关联的冷数据。

 

 

那这个类看起来像什么呢?

 

 

该实现基于全局映射的思想,该全局映射隐藏在一个地方,这个地方将指向快速对象的指针映射到冷对象的指针上。

 

 

你可以从任何你能建立冷数据的东西建立这个基础。当你这么做的时候,它会创建冷数据并把它和你的快速对象联系起来。
 

 

当您的快速对象被清理后,相应的冷对象也会:

 

 

当您移动快速对象时,相应的冷对象将与新的快速对象重新关联(请记住,这意味着您不应该在移出对象上使用冷数据)。


 

 

为了简单起见,当前的实现只是使OutOfLine不可复制,但是可以选择通过复制冷数据来实现复制构造。

 

 

不过现在为了使其对我们有用,实际上必须使得访问这些冷数据十分方便才行。当您从OutOfLine继承时,您的类将获得常量和非常量成员函数cold():


 

 

调用这些会给你一个(适当的常量)你的冷数据参考。

就这样。这个解除链接的文件只有4B大,它可以全速访问FD成员,并且仍然保持所有相同的RAII行为。所有与生命时长有关的工作都是为你处理的。当你移动快速对象时,冷对象会重新关联到新的快速对象。当你的快速物体离开时,冷物体也会离开。

 

然而,有时您的数据会让您的生活变得麻烦,而且必须首先构造快速数据,因为它是冷数据的构造器参数。这使得结构顺序与OutOfLine的顺序相反。此外,有时您需要快速数据来延长冷数据的寿命(可能冷数据包含对快速数据的引用)。对于这些情况,我们需要一个“安全舱口”来控制数据初始化和取消初始化的顺序。
 

 

您的类可以调用OutOfLine的另一个构造函数,这个构造函数接受TwoPhaseInit类型的标记。如果你以这种方式构建你的OutOfLine,你的冷数据将不会被初始化,且你将会处于一个半构建状态。然后,通过调用init_cold_data(用任何可以构造ColdData的参数)完成两阶段构造,就可以完成了。但是请记住不要对尚未初始化冷数据的对象调用cold()。如果您的数据需要,您可以通过调用release_cold_data来提前发布冷数据。
 

 

这就是所有内容。那么最终,我们的29 SLOC为我们带来什么?它在我们权衡时又给了我们一个选择。无论何时当你有一个对象,其中一些成员比其他成员重要得多,你可能会考虑OutOfLine。它可以让你让一些成员访问速度更快一点,但代价是让访问其他成员的速度慢很多,所以对你来说这算是一个好的折衷的情况下,你便可以选择这么做。

 

我们已经能够在几个地方应用这种技术——在关机或者在罕见或意外的情况下,用额外的元数据标记快速数据是相当常见的。无论是记录该连接属于哪个用户,该订单属于哪个内部交易台,还是记录硬件加速的市场数据,这都将在您处于关键路径时保持缓存行整洁。
 

我已经放入了一个基准测试在其中,您可以使用它来查看和探索差异。


 

 

我用OutOfLine测量了大约10倍的加速比。显然,这个基准测试是为了提供OutOfLine的最佳应用场景,但是它证明了缓存优化可以产生非常实际的性能影响,而且确实OutOfLine在这方面有所贡献。另外,保持数据缓存中不受冷数据影响,也可能对代码的其余部分带来难以衡量的整体性好处。像往常一样,您需要测量每个应用程序来优化它,但这可能是对你来说非常有用的工具了。

 

欢迎加入FinTech专设的C++讨论社群,可添加微信janelj78。