我尝试创建一个本地数组,包含一些POD值(例如
如果上述问题确实是由C++标准强制引起的(而不是编译器编写者的错),那么这样的
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选项,从Clang生成的 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
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++标准确实需要这种行为,每当我们将数组及其大小放入同一类中时,就会导致生成效率较低的代码?
P.S.
我意识到我上面的代码示例可能有点牵强。但请考虑以下情况:我想在我的代码中使用轻量级的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++语言来说是相当糟糕的。