当前位置: 首页 > 知识库问答 >
问题:

为什么C编译器不优化结构数据成员的读写,而不是不同的局部变量?

暨成双
2023-03-14

我正在尝试创建一些POD值的本地数组(例如,double),其中包含编译时已知的固定max_sizevalue(size

问题是,当arr大小被放入同一个结构/中时,为什么编译器不消除堆栈读取和写入,而不是arrsize是独立的局部变量的情况?

这是我的代码:

#include <cstddef>
constexpr std::size_t max_size = 64;

extern void process_value(double& ref_value);

void test_distinct_array_and_size(std::size_t size)
{
    double arr[max_size];
    std::size_t arr_size = size;

    for (std::size_t i = 0; i < arr_size; ++i)
        process_value(arr[i]);
}

void test_array_and_size_in_local_struct(std::size_t size)
{
    struct
    {
        double arr[max_size];
        std::size_t size;
    } array_wrapper;
    array_wrapper.size = size;

    for (std::size_t i = 0; i < array_wrapper.size; ++i)
        process_value(array_wrapper.arr[i]);
}

带有-O3的Clang中的< code > test _ distinct _ array _ and _ size 的汇编输出:

test_distinct_array_and_size(unsigned long): # @test_distinct_array_and_size(unsigned long)
  push r14
  push rbx
  sub rsp, 520
  mov r14, rdi
  test r14, r14
  je .LBB0_3
  mov rbx, rsp
.LBB0_2: # =>This Inner Loop Header: Depth=1
  mov rdi, rbx
  call process_value(double&)
  add rbx, 8
  dec r14
  jne .LBB0_2
.LBB0_3:
  add rsp, 520
  pop rbx
  pop r14
  ret

< code > test _ array _ and _ size _ in _ local _ struct 的程序集输出:

test_array_and_size_in_local_struct(unsigned long): # @test_array_and_size_in_local_struct(unsigned long)
  push r14
  push rbx
  sub rsp, 520
  mov qword ptr [rsp + 512], rdi
  test rdi, rdi
  je .LBB1_3
  mov r14, rsp
  xor ebx, ebx
.LBB1_2: # =>This Inner Loop Header: Depth=1
  mov rdi, r14
  call process_value(double&)
  inc rbx
  add r14, 8
  cmp rbx, qword ptr [rsp + 512]
  jb .LBB1_2
.LBB1_3:
  add rsp, 520
  pop rbx
  pop r14
  ret

最新的GCC和MSVC编译器对栈的读写做了基本相同的事情。

如我们所见,读取并写入array_wrapper。在后一种情况下,堆栈上的size变量不会被优化掉。在循环开始之前,size值被写入位置[rsp 512]迭代之后,从该位置读取。

因此,编译器有点希望我们修改array_wrapper。processvalue(array_wrapper.arr[i])调用中的size

但是,如果我们从那个电话中尝试这样做,那难道不是未定义的行为吗?

当我们以以下方式重写循环时

for (std::size_t i = 0, sz = array_wrapper.size; i < sz; ++i)
    process_value(array_wrapper.arr[i]);

每次迭代结束时,那些不必要的读取将会消失。但是对< code>[rsp 512]的初始写入将会保留,这意味着编译器仍然希望我们能够从这些< code>process_value调用中访问该位置的< code>array_wrapper.size变量(通过执行一些基于偏移量的奇怪操作)。

为什么?

这只是现代编译器实现中的一个小缺点吗(希望很快就会得到修复)?或者C标准确实需要这样的行为,当我们将数组及其大小放入同一个类时,这种行为会导致生成效率较低的代码吗?

附言

我意识到我上面的代码示例可能看起来有点人为的。但请考虑一下:我想在我的代码中使用一个轻量级的boost::container::static_vector类模板,以便使用POD元素的伪动态数组进行更安全,更方便的“C风格”操作。因此,我的 POD 矢量将包含同一类中的数组和size_t

template<typename T, std::size_t MaxSize>
class PODVector
{
    static_assert(std::is_pod<T>::value, "T must be a POD type");

private:
    T _data[MaxSize];
    std::size_t _size = 0;

public:
    using iterator = T *;

public:
    static constexpr std::size_t capacity() noexcept
    {
        return MaxSize;
    }

    constexpr PODVector() noexcept = default;

    explicit constexpr PODVector(std::size_t initial_size)
        : _size(initial_size)
    {
        assert(initial_size <= capacity());
    }

    constexpr std::size_t size() const noexcept
    {
        return _size;
    }

    constexpr void resize(std::size_t new_size)
    {
        assert(new_size <= capacity());
        _size = new_size;
    }

    constexpr iterator begin() noexcept
    {
        return _data;
    }

    constexpr iterator end() noexcept
    {
        return _data + _size;
    }

    constexpr T & operator[](std::size_t position)
    {
        assert(position < _size);
        return _data[position];
    }
};

用法:

void test_pod_vector(std::size_t size)
{
    PODVector<double, max_size> arr(size);

    for (double& val : arr)
        process_value(val);
}

如果上面描述的问题确实是由C标准强制的(并且不是编译器编写者的错),这样的PODVector将永远不会像原始使用数组和大小的“不相关”变量那样有效。这对于C作为一种想要零开销抽象的语言来说是非常糟糕的。

共有1个答案

麹高远
2023-03-14

这是因为< code>void process_value(double

编译器假设,因为< code>array和< code>size是同一个对象的成员,所以< code>array_wrapper函数< code>process_value可能会将对第一个元素的引用(在第一次调用时)转换为对该对象的引用(并将其存储在其他地方),并将该对象转换为< code>unsigned char并读取或替换其整个表示形式。因此在函数返回后,对象的状态必须从内存中重新加载。

size 是堆栈上的独立对象时,编译器/优化器假定没有其他对象可能具有对它的引用/指针,并将其缓存在寄存器中。

在Chandler Carruth:优化C语言的涌现结构中,他解释了为什么优化器在调用接受引用/指针参数的函数时会有困难。仅在绝对必要时使用引用/指针函数参数。

如果您想更改值,更高性能的选项是:

double process_value(double value);

然后:

array_wrapper.arr[i] = process_value(array_wrapper.arr[i]);

此更改导致最佳装配:

.L23:
movsd xmm0, QWORD PTR [rbx]
add rbx, 8
call process_value2(double)
movsd QWORD PTR [rbx-8], xmm0
cmp rbx, rbp
jne .L23

或者:

for(double& val : arr)
    val = process_value(val);
 类似资料:
  • 我最近一直在尝试重新熟悉多线程,并找到了这篇论文。其中一个例子说在使用类似这样的代码时要小心: 声明: 是作者写错了,还是我漏掉了什么?

  • (这个问题与此密切相关,但它是一个更具体的问题,我希望能就此得到答案)

  • 考虑下面的代码片段 通过-O3优化,最新的gcc和clang版本没有优化指向包装器的指针、指向底层函数的指针。参见第22行的组装: 在后面的+中,编译器将指针放置到,而不只是。 编辑2。同样模式的更简单的例子: gcc 8.2通过抛出指向包装器的指针并将直接存储在其位置(https://gcc.godbolt.org/z/nmibeo)成功地优化了这段代码。然而,按照注释更改代码行(即手动执行相同

  • 我经常注意到gcc在可执行文件中将乘法转换为移位。当将与相乘时,可能会发生类似的情况。例如,可能只是将的指数增加1,从而节省一些周期。如果有人要求编译器这样做(例如,通过),编译器通常会这样做吗? 编译器通常是否足够聪明来执行此操作,还是我需要自己使用或函数系列来执行此操作?

  • 问题内容: Java编译器了解if语句的条件始终为true,因此y将始终被初始化。没有编译错误,如预期的那样。 但是,当我将x的声明和初始化分为两行时,编译器似乎没有得到条件始终为true且y将始终被初始化的信息。 同样的事情在这里发生,编译器会损失精度误差。 同样,编译器可以理解x在b的范围内。 问题答案: 它与编译器如何确定是否执行语句有关。它在JLS#16中定义: 每个局部变量和每个空白的f

  • 我有以下一段代码: 我可以想象编译器具有消除函数中的子句所需的所有信息。调用该函数的唯一代码路径来自main,它接受,因此它应该能够确定该代码路径未被使用。但是: 在GCC 12.2中,我们看到第二部分链接到。 如果我函数,这将消失: 我在这里错过了什么?有没有办法让编译器做一些更智能的工作?这发生在带有 C 和 。