POD 数学结构 类 的按值传递与按引用传递的 C++ 选择,适用于考虑缓存一致性的高性能应用程序
C++ choice of pass by value vs pass by reference for POD math structure classes for high performance applications considering cache coherency
对于许多高性能应用程序,例如游戏引擎或财务软件,考虑缓存一致性、内存布局和缓存未命中对于保持平稳性能至关重要。随着 C++ 标准的发展,尤其是随着 Move Semantics 和 C++14 的引入,何时划清界线变得越来越不明确基于数学 POD 的按值传递与按引用传递的对比 classes.
考虑常见的 POD Vector3 class:
class Vector3
{
public:
float32 x;
float32 y;
float32 z;
// Implementation Functions below (all non-virtual)...
}
这是游戏开发中最常用的数学结构。它是一个 非虚拟 ,12 字节大小 class,即使在 64 位中也是如此,因为我们明确使用 IEEE float32,每个浮点数使用 4 个字节。我的问题如下 - 在决定通过高性能应用程序的 POD 数学 classes 按值或按引用传递时,一般最佳实践指南是什么?
回答这个问题时的一些注意事项:
- 假设默认构造函数不初始化任何值是安全的
- 可以安全地假设没有超过一维的数组用于任何 POD 数学结构
- 显然大多数人都是按值传递 4-8 字节的 POD 常量,所以似乎没有太多争论
- 当此 Vector 是 class 成员变量而不是堆栈上的局部变量时会发生什么?如果使用引用传递,那么它将使用 class 上变量的内存地址,而不是堆栈上本地某物的内存地址。这个用例重要吗?使用 PBR 的这种差异会导致更多缓存未命中吗?
- 使用或不使用SIMD的情况如何?
- 移动语义编译器优化怎么样?我注意到,当切换到 C++14 时,当链函数调用按值传递相同的向量时,编译器通常会使用移动语义,尤其是当它是 const 时。我通过仔细阅读程序集故障观察到这一点
- 当对这些数学结构使用按值传递和按引用传递时,const 对编译器优化有很大影响吗?见上一点
鉴于上述情况,对于现代 C++ 编译器(C++14 及更高版本)何时使用按值传递与按引用传递以最大程度地减少缓存未命中并提高缓存一致性,有什么好的指南?在什么时候有人可能会说这个 POD 数学结构对于按值传递来说太大了,例如 4v4 仿射变换矩阵,假设使用 float32,它的大小为 64 字节。在做出此决定时,Vector,或者更确切地说任何小型 POD 数学结构,在堆栈上声明与作为成员变量引用是否重要?
我希望有人可以提供一些分析和见解,以便为上述情况建立良好的现代最佳实践指南。我相信随着 C++ 标准的发展,何时使用 PBV 与 PBR 用于 POD classes 的界线变得更加模糊,特别是在最大限度地减少缓存未命中方面。
我看到问题的标题是关于 pass-by-value 与 pass-by-reference 的选择,尽管听起来您更广泛地追求的是有效传递 3D 向量和其他向量的最佳实践常见 PODs。传递数据是基础,并且与编程范式交织在一起,因此没有就最佳方式达成共识。除了性能之外,还需要权衡代码可读性、灵活性和可移植性等因素,以决定在给定应用程序中采用哪种方法。
也就是说,近年来,"data-oriented design" has become a popular alternative to object-oriented programming, especially in video game development. The essential idea is to think about the program in terms of data it needs to process, and how all that data can be organized in memory for good cache locality and computation performance. There was a great talk about it at CppCon 2014: "Data-Oriented Design and C++" by Mike Acton。
以您的 Vector3 示例为例,通常情况下,一个程序不仅有一个而是多个 3D 矢量,它们都以相同的方式处理,比如说,都经过相同的几何变换。 Data-oriented 设计表明,将向量连续放置在内存中是一个好主意,并且它们都在批处理操作中一起转换。这改进了缓存并创造了利用 SIMD 指令的机会。您可以使用 Eigen C++ linear algebra library 实现此示例。可以使用形状为 3xN 的 Eigen::Matrix<float, 3, Eigen::Dynamic>
来表示向量以存储 N 个向量,然后使用 Eigen 的 SIMD-accelerated 操作进行操作。
对于许多高性能应用程序,例如游戏引擎或财务软件,考虑缓存一致性、内存布局和缓存未命中对于保持平稳性能至关重要。随着 C++ 标准的发展,尤其是随着 Move Semantics 和 C++14 的引入,何时划清界线变得越来越不明确基于数学 POD 的按值传递与按引用传递的对比 classes.
考虑常见的 POD Vector3 class:
class Vector3
{
public:
float32 x;
float32 y;
float32 z;
// Implementation Functions below (all non-virtual)...
}
这是游戏开发中最常用的数学结构。它是一个 非虚拟 ,12 字节大小 class,即使在 64 位中也是如此,因为我们明确使用 IEEE float32,每个浮点数使用 4 个字节。我的问题如下 - 在决定通过高性能应用程序的 POD 数学 classes 按值或按引用传递时,一般最佳实践指南是什么?
回答这个问题时的一些注意事项:
- 假设默认构造函数不初始化任何值是安全的
- 可以安全地假设没有超过一维的数组用于任何 POD 数学结构
- 显然大多数人都是按值传递 4-8 字节的 POD 常量,所以似乎没有太多争论
- 当此 Vector 是 class 成员变量而不是堆栈上的局部变量时会发生什么?如果使用引用传递,那么它将使用 class 上变量的内存地址,而不是堆栈上本地某物的内存地址。这个用例重要吗?使用 PBR 的这种差异会导致更多缓存未命中吗?
- 使用或不使用SIMD的情况如何?
- 移动语义编译器优化怎么样?我注意到,当切换到 C++14 时,当链函数调用按值传递相同的向量时,编译器通常会使用移动语义,尤其是当它是 const 时。我通过仔细阅读程序集故障观察到这一点
- 当对这些数学结构使用按值传递和按引用传递时,const 对编译器优化有很大影响吗?见上一点
鉴于上述情况,对于现代 C++ 编译器(C++14 及更高版本)何时使用按值传递与按引用传递以最大程度地减少缓存未命中并提高缓存一致性,有什么好的指南?在什么时候有人可能会说这个 POD 数学结构对于按值传递来说太大了,例如 4v4 仿射变换矩阵,假设使用 float32,它的大小为 64 字节。在做出此决定时,Vector,或者更确切地说任何小型 POD 数学结构,在堆栈上声明与作为成员变量引用是否重要?
我希望有人可以提供一些分析和见解,以便为上述情况建立良好的现代最佳实践指南。我相信随着 C++ 标准的发展,何时使用 PBV 与 PBR 用于 POD classes 的界线变得更加模糊,特别是在最大限度地减少缓存未命中方面。
我看到问题的标题是关于 pass-by-value 与 pass-by-reference 的选择,尽管听起来您更广泛地追求的是有效传递 3D 向量和其他向量的最佳实践常见 PODs。传递数据是基础,并且与编程范式交织在一起,因此没有就最佳方式达成共识。除了性能之外,还需要权衡代码可读性、灵活性和可移植性等因素,以决定在给定应用程序中采用哪种方法。
也就是说,近年来,"data-oriented design" has become a popular alternative to object-oriented programming, especially in video game development. The essential idea is to think about the program in terms of data it needs to process, and how all that data can be organized in memory for good cache locality and computation performance. There was a great talk about it at CppCon 2014: "Data-Oriented Design and C++" by Mike Acton。
以您的 Vector3 示例为例,通常情况下,一个程序不仅有一个而是多个 3D 矢量,它们都以相同的方式处理,比如说,都经过相同的几何变换。 Data-oriented 设计表明,将向量连续放置在内存中是一个好主意,并且它们都在批处理操作中一起转换。这改进了缓存并创造了利用 SIMD 指令的机会。您可以使用 Eigen C++ linear algebra library 实现此示例。可以使用形状为 3xN 的 Eigen::Matrix<float, 3, Eigen::Dynamic>
来表示向量以存储 N 个向量,然后使用 Eigen 的 SIMD-accelerated 操作进行操作。