C++ 加载和存储优化和堆对象

C++ load and store optimizations and heap objects

我正在努力解决对已加载或未加载到寄存器的内部类型的内存访问。

假设一些 SIMD 函数接受对浮点数组的引用。例如,

void do_something(std::array<float, 4>& arr);
void do_something_else(std::array<float, 4>& arr);

每个函数首先将数据加载到寄存器中,执行其操作,然后将结果存储回数组中。假设以下片段:

std::array<float, 4> my_arr{0.f, 0.f, 0.f, 0.f};
do_something(my_arr);
do_something_else(my_arr);
do_something(my_arr);

C++ 编译器是否优化了函数调用之间不必要的加载和存储?这有关系吗?

我见过将 __m128 类型包装在结构中并在构造函数中调用加载的库。当您将这些存储在堆上并尝试调用它们的内在函数时会发生什么?例如,

struct vec4 {
    vec4(std::array<float, 4>&) {
        // do load
    }

    __m128 data;
};

std::vector<vec4> my_vecs;
// do SIMD work

每次访问都必须 load/store 数据吗?或者这些 类 是否应该声明一个私有的 operator new,这样它们就不会存储在堆中?

如果编译器将函数与调用分开编译,则无法优化存储和加载。当函数在一个 .cpp 文件中,调用在另一个 .cpp 文件中,并且未启用 link 时间优化时,情况确实如此。

但是,如果编译器

  1. 同时(或在link时间优化期间)看到函数定义及其调用,

  2. 决定内联函数调用并且

  3. 决定熔断循环,

然后它可能会删除不必要的存储和负载。

但是请注意,这三个点中的 none 是微不足道的。程序员只控制第一点,其他两点由编译器自行决定 100%。因此,您通常必须假设此类优化不会发生。如果您的函数实际上是模板(这也保证满足第 1 点),内联的可能性会增加一点,但是编译器是否真的融合循环是您无法控制的。


关于包含 SIMD 类型的结构:SIMD 类型驻留在堆上是完全合法的。和分配到栈上完全没有区别。

但是,您不能只使用 __m128 作为 std::array<float, 4> 的别名,那样会违反严格的别名规则。将 std::array<float, 4> 重新解释为 __m128 只能通过副本安全地发生(重新解释为 char*,复制,重新解释为 __m128),否则允许您的编译器混淆访问到数组和 SIMD 类型。