为什么C++编译器不优化对结构数据成员的读取和写入,而不是不同的局部变量?
Why don't C++ compilers optimize away reads and writes to struct data members as opposed to distinct local variables?
我正在尝试创建一些 POD 值的本地数组(例如double
) 使用编译时已知的固定max_size
,然后读取运行时size
值 (size <= max_size
) 并首先处理该数组中的size
元素。
问题是,当arr
和size
放入同一个struct
/class
中时,为什么编译器不消除堆栈读写,而不是arr
和size
是独立的局部变量的情况?
这是我的代码:
#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 从 Clangtest_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
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]
,并在每次迭代后从该位置读取。
因此,编译器有点期望我们想要从process_value(array_wrapper.arr[i])
调用中修改array_wrapper.size
(通过获取当前数组元素的地址并对其应用一些奇怪的偏移量?
但是,如果我们试图从那个调用中这样做,那不是未定义的行为吗?
当我们以下列方式重写循环时
for (std::size_t i = 0, sz = array_wrapper.size; i < sz; ++i)
process_value(array_wrapper.arr[i]);
,每次迭代结束时那些不必要的读取都将消失。但是对[rsp + 512]
的初始写入将保留,这意味着编译器仍然希望我们能够从这些process_value
调用(通过执行一些奇怪的基于偏移量的魔法)访问该位置的array_wrapper.size
变量。
为什么?
这是否只是现代编译器实现中的一个小缺点(希望很快就会修复)?或者C++标准是否确实需要这样的行为,每当我们将数组及其大小放入同一类时,都会生成效率较低的代码?
附言
我意识到我上面的代码示例似乎有点做作。但请考虑一下:我想在我的代码中使用一个轻量级boost::container::static_vector
类模板,以便使用 POD 元素的伪动态数组进行更安全、更方便的"C++ 式"操作。所以我的PODVector
将包含同一个类中的一个数组和一个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++作为一种想要零开销抽象的语言来说是非常糟糕的。
这是因为void process_value(double& ref_value);
通过引用接受参数。编译器/优化器假定别名,即process_value
函数可以更改可通过引用ref_value
访问的内存,从而更改数组之后size
成员。
编译器假定由于array
和size
是同一对象array_wrapper
函数的成员,process_value
可能会将对第一个元素的引用(在第一次调用时)强制转换为对对象的引用(并将其存储在其他位置),并将对象强制转换为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);
- Gnuplot_i.hpp C++接口绘制局部变量而不是文件
- 赋予全局变量而不是局部变量优先级的函数 - (异常行为)
- 将共享指针传递给函数参数 - 将其分配给局部变量的正确方法是什么
- 为什么 lambda 对象中的局部变量是常量变量?
- 局部变量的作用域是块或函数
- 如果两个线程调用同一个函数,但函数中的所有变量都是局部变量,我还需要担心线程之间共享数据吗?
- 我正在比较两个具有相同值的变量,但它说它们是不同的
- 是包含线程局部变量重入的函数
- 为什么C++编译器不优化对结构数据成员的读取和写入,而不是不同的局部变量?
- C++11 是否保证 return 语句中的局部变量将被移动而不是复制?
- 编译器是否可以从全局变量中读取两次,而不是存储一个局部变量
- 我不知道如何让我的程序使用局部变量而不是全局变量
- 怎么可能有两个同名的变量——一个是全局变量,另一个是局部变量
- 只有当循环中更新的变量是局部变量时,计算才会优化
- 编译器如何知道变量是全局变量还是局部变量(C)
- 当变量是局部变量时,是否需要删除
- 为什么由指针和对象构建的局部变量显示不同的输出
- 在函数中破坏局部变量是什么意思
- 要创建持久变量,最好是使用局部静态变量还是全局变量
- 为什么局部变量的地址对于不同的执行是相同的