考虑到缓存一致性的高性能应用程序的POD数学结构类的C ++选择按值传递还是按引用传递

问题描述

对于许多高性能应用程序(例如游戏引擎或财务软件),对缓存一致性,内存布局和缓存未命中的考虑对于保持流畅的性能至关重要。随着C ++标准的发展,特别是随着 Move Semantics C ++ 14 的引入,何时划定值与值之间的界限变得越来越不清楚。通过引用传递基于数学POD的类。

考虑常见的POD Vector3类:

class Vector3
{
public:
   float32 x;
   float32 y;
   float32 z;
   // Implementation Functions below (all non-virtual)...
}

这是游戏开发中最常用的数学结构。这是一个非虚拟的12字节大小的类,即使在64位中也是如此,因为我们明确使用了IEEE float32,每个float都使用4个字节。我的问题如下-决定为高性能应用程序按值或按引用传递POD数学类时,要使用的一般最佳实践准则是什么?

回答此问题时需要考虑的一些事项:

  • 可以安全地假定默认构造函数未初始化任何值
  • 可以安全地假设任何POD数学结构均不使用超过1D的数组
  • 很明显,大多数人都按值传递4-8字节的POD常量,因此似乎没有太多争论。
  • 当此Vector是堆栈中的类成员变量与局部变量时,会发生什么?如果使用按引用传递,则它将使用该类上变量的内存地址与堆栈上本地变量的内存地址。这个用例重要吗?使用PBR的这种区别会导致更多的缓存未命中吗?
  • 使用或不使用SIMD的情况如何?
  • 如何移动语义编译器优化?我已经注意到,当切换到C ++ 14时,当链函数调用通过值传递相同的矢量时,尤其是在const时,编译器将经常使用move语义。我通过仔细查看组装故障来观察到这一点
  • 在这些数学结构中使用按值传递和按引用传递时, const 是否会对编译器优化产生很大影响?见上述要点

鉴于上述情况,什么时候在现代C ++编译器(C ++ 14及更高版本)中使用按值传递与按引用传递来减少高速缓存未命中并提高高速缓存一致性的良好指导方针是什么?有人会在什么时候说此POD数学结构太大而无法按值传递,例如4v4仿射变换矩阵,假设使用float32,它的大小为64字节。在做出此决定时,在堆栈上声明的Vector或任何小的POD数学结构与被引用为成员变量是否重要?

我希望有人可以提供一些分析和见解,以针对上述情况建立最佳的现代最佳实践指南。我相信随着C ++标准的发展,关于何时使用PBV和PBR进行POD类的界限已经变得越来越模糊,尤其是在最大程度地减少缓存丢失方面。

解决方法

我看到问题的标题是关于按值传递还是按引用传递的选择,尽管听起来更广泛地讲,这是有效传递3D向量和其他常见POD的最佳实践。传递数据是基本的,并且与编程范式交织在一起,因此,关于最佳处理方式尚未达成共识。除了性能之外,还需要权衡诸如代码的可读性,灵活性和可移植性等因素,以决定在给定应用程序中偏爱哪种方法。

也就是说,"data-oriented design"近年来已成为面向对象编程的流行替代方法,尤其是在视频游戏开发中。基本思想是根据程序需要处理的数据来考虑程序,以及如何在内存中组织所有这些数据以实现良好的缓存局部性和计算性能。在2014年CppCon上有一个很好的话题:"Data-Oriented Design and C++" by Mike Acton

例如,在您的Vector3示例中,通常情况是程序不仅具有一个3D矢量,而且许多3D矢量都以相同的方式处理,例如,它们都经历相同的几何变换。面向数据的设计表明,最好将向量连续地放置在内存中,并在批处理操作中将它们全部转换在一起。这改善了缓存,并创造了利用SIMD指令的机会。您可以使用Eigen C++ linear algebra library来实现此示例。可以使用形状为3xN的Eigen::Matrix<float,3,Eigen::Dynamic>表示矢量来存储N个矢量,然后使用Eigen的SIMD加速操作对其进行处理。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...