为什么用 constexpr 数组编译头文件会占用这么多内存?

Why does compiling a header file with constexpr array use so much memory?

我有一个头文件,其中包含一个包含一些原始二进制数据的无符号字符数组:

#include <string>
#include <array>
extern __declspec(selectany) inline constexpr std::string_view bin_name = std::string_view("test.bin");
extern __declspec(selectany) inline constexpr int bin_size = 10812406;
extern __declspec(selectany) inline constexpr std::array<unsigned char, 10812406> bin_data = {...}

我有另一个头文件获取该数组并将其写入文件:

#include "_.h"
#include <fstream>
void write_file(std::string const outputDir = ".") {
    std::ofstream file;
    file.open(outputDir + "/" + bin_name.data(), std::ios::out | std::ios::binary);
    file.write((const char*)& bin_data[0], bin_size);
    file.close();
}

最后在 main 中我只调用函数来写入文件:

#include "_.h"

int main()
{
    write_file();
}

尝试在 Visual Studio 中编译以上内容,或直接使用 cl (cl /Os /std:c++17 /Zc:externConstexpr main.cpp) 导致编译器使用超过 12 GB 的内存!

bin_data 的大小只有 10 MB 左右,因此编译器分配的 >12GB 似乎有些矫枉过正。更重要的是,我想使用的二进制数据的实际大小约为 50 MB,但是正如您可能已经猜到的那样,编译超过了我的系统 32GB 内存,因此我不能这样做。

所以我的问题是:

编译器不太适合大型初始化器,因为 go-to 编译初始化器的方法是为每个元素创建一个 AST 节点。

a proposal 可以直接用包括动机和备选方案的语言来处理这个问题:

constexpr std::span<const std::byte> bin_data = std::embed("test.bin");

该提案的作者还写了a blog post来了解目前的情况并进一步为该提案辩护。

备选方案并不是真的很好。一种技术是将大型初始化程序放入其自己的翻译单元中以避免重新编译它。然而,这并不能扩展——您的 too-large 初始化程序需要适当地拆分以限制任何特定 TU 的内存使用,而且这仍然不能很好地与并行构建一起使用。

另一种技术是在字符串文字中对数据进行编码,然后对其进行解析以获取您要查找的类型。该提案提到 MSVC 不允许大字符串文字,我不知道这在过去几年中是否有所改变。无论数据文件是直接包含字符串文字还是用于在构建步骤中生成字符串文字,都不是很好。

然后是性能更好的替代方案,但相当 non-portable 并且可能需要大量的努力和丑陋才能使 portable 足够。这涉及将数据直接放置在链接到的文件中,使用使之可行的工具和访问数据的符号。这些也消除了在常量表达式中使用数据的可能性。

总而言之,我还没有遇到一个没有重大包袱的替代方案。从业务角度来看,我会非常诚实地认为,从长远来看,支持一个好的解决方案的标准化工作是值得的 运行。为编译器对大型初始值设定项的处理进行优化是另一种途径,但肯定不是一项微不足道的任务。 Circle 作为博客 post 基准测试的一部分取得了一些成功,但这并不意味着将结果转移到其他编译器很容易,MSVC 完全不在 table 那里。