了解 C++ constexpr 性能
Understanding C++ constexpr Performance
我最近使用 constexpr
函数和 C++17 编写了一个编译时光线追踪器。完整源码可见here。这个问题的相关代码如下所示:
constexpr auto image = []() {
StaticImage<image_width, image_height> image;
Camera camera{Pointf{0.0f, 0.0f, 500.0f},
Vectorf{0.0f},
Vectorf{0.0f, 1.0f, 0.0f},
500.0f};
std::array<Shapes, 1> shapes_list{Sphere{Pointf{0.0f}, 150.0f}};
std::array<Materials, 1> materials_list{DefaultMaterial{}};
ShapeContainer<decltype(shapes_list)> shapes{std::move(shapes_list)};
MaterialContainer<decltype(materials_list)> materials{
std::move(materials_list)};
SphereScene scene;
scene.set_camera(camera);
Renderer::render(scene, image, shapes, materials);
return image;
}();
此处显示的每个 class(StaticImage
、Camera
、Shapes
、Materials
、ShapeContainer
、MaterialContainer
, 和 SphereScene
) 完全由 constexpr
函数组成。 Renderer::render
也是 constexpr
并且负责遍历图像中的每个像素,将光线射入场景,并设置相应的颜色。
使用当前设置和 512x512 图像,在发布模式下使用 MSVC 16.9.2,编译器大约需要 35 分钟才能完成图像生成。在此过程中,其内存使用量上升到最终使用近 64GB RAM 的程度。
所以,我的问题是:为什么编译时间和内存使用率这么高?
我的理论是编译时间的部分原因是调用堆栈的复杂性(即大量模板、CRTP 和深度),所以我尝试通过删除几个模板来稍微简化调用堆栈(例如,Vector
class 不再模板化)并设法将编译时间减少到 32 分钟,内存使用量减少到 61GB。更好,但仍然很高。问题是我不太明白为什么它这么慢。我知道评估所有 constexpr
函数是一个非常复杂的过程(因为编译器必须检查 UB、类型推导等),但我没想到它会这么慢。我也对高内存使用率感到困惑。图像数组本身使用的内存不超过 4MB (512 * 512 * 3 * sizeof(float)
) 那么额外的内存从何而来?
编译时执行将比运行时执行效率低很多。编译器必须做更多的工作才能执行相同的代码。编译时执行的要点是进行您无法在运行时进行的计算。有时,为了编译时缓存更简单的计算。
编写仅在编译时存在的完整、重要的应用程序不会很快完成。
至于细节,成本增加的主要原因是编译时执行必须检测所有未定义的行为。这意味着很多可能只是抵消指针的事情必须更加复杂。堆栈变量不能只是偏移堆栈指针;他们必须明确地跟踪对象的生命周期。等等。
编译时执行基本上是解释C++。并且没有太多理由让它成为特别快速的解释器。大多数编译时操作都处理基于类型和简单值的计算,而不是复杂的数据结构。这就是编译器主要针对的优化。
我记得最近有一些声音通过更好的解释来改进 Clang 的 constexpr
执行。但是我不知道到底有多少。
我最近使用 constexpr
函数和 C++17 编写了一个编译时光线追踪器。完整源码可见here。这个问题的相关代码如下所示:
constexpr auto image = []() {
StaticImage<image_width, image_height> image;
Camera camera{Pointf{0.0f, 0.0f, 500.0f},
Vectorf{0.0f},
Vectorf{0.0f, 1.0f, 0.0f},
500.0f};
std::array<Shapes, 1> shapes_list{Sphere{Pointf{0.0f}, 150.0f}};
std::array<Materials, 1> materials_list{DefaultMaterial{}};
ShapeContainer<decltype(shapes_list)> shapes{std::move(shapes_list)};
MaterialContainer<decltype(materials_list)> materials{
std::move(materials_list)};
SphereScene scene;
scene.set_camera(camera);
Renderer::render(scene, image, shapes, materials);
return image;
}();
此处显示的每个 class(StaticImage
、Camera
、Shapes
、Materials
、ShapeContainer
、MaterialContainer
, 和 SphereScene
) 完全由 constexpr
函数组成。 Renderer::render
也是 constexpr
并且负责遍历图像中的每个像素,将光线射入场景,并设置相应的颜色。
使用当前设置和 512x512 图像,在发布模式下使用 MSVC 16.9.2,编译器大约需要 35 分钟才能完成图像生成。在此过程中,其内存使用量上升到最终使用近 64GB RAM 的程度。
所以,我的问题是:为什么编译时间和内存使用率这么高?
我的理论是编译时间的部分原因是调用堆栈的复杂性(即大量模板、CRTP 和深度),所以我尝试通过删除几个模板来稍微简化调用堆栈(例如,Vector
class 不再模板化)并设法将编译时间减少到 32 分钟,内存使用量减少到 61GB。更好,但仍然很高。问题是我不太明白为什么它这么慢。我知道评估所有 constexpr
函数是一个非常复杂的过程(因为编译器必须检查 UB、类型推导等),但我没想到它会这么慢。我也对高内存使用率感到困惑。图像数组本身使用的内存不超过 4MB (512 * 512 * 3 * sizeof(float)
) 那么额外的内存从何而来?
编译时执行将比运行时执行效率低很多。编译器必须做更多的工作才能执行相同的代码。编译时执行的要点是进行您无法在运行时进行的计算。有时,为了编译时缓存更简单的计算。
编写仅在编译时存在的完整、重要的应用程序不会很快完成。
至于细节,成本增加的主要原因是编译时执行必须检测所有未定义的行为。这意味着很多可能只是抵消指针的事情必须更加复杂。堆栈变量不能只是偏移堆栈指针;他们必须明确地跟踪对象的生命周期。等等。
编译时执行基本上是解释C++。并且没有太多理由让它成为特别快速的解释器。大多数编译时操作都处理基于类型和简单值的计算,而不是复杂的数据结构。这就是编译器主要针对的优化。
我记得最近有一些声音通过更好的解释来改进 Clang 的 constexpr
执行。但是我不知道到底有多少。