Tar*_*ras 7 c++ arrays optimization boost compilation
我正在尝试创建一些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]);
}
Run Code Online (Sandbox Code Playgroud)
test_distinct_array_and_size使用-O3从Clang 组装输出:
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
Run Code Online (Sandbox Code Playgroud)
装配输出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
Run Code Online (Sandbox Code Playgroud)
最新的GCC和MSVC编译器与堆栈读写基本相同.
正如我们所看到的,array_wrapper.size在后一种情况下,对堆栈上的变量的读取和写入不会被优化.还有就是写size值到位置[rsp + 512]的循环开始之前,以及之后的位置读取每个迭代.
那么,编译器有点期望我们想要array_wrapper.size从process_value(array_wrapper.arr[i])调用中修改(通过获取当前数组元素的地址并对它应用一些奇怪的偏移量?)
但是,如果我们试图通过该调用这样做,那不是未定义的行为吗?
当我们以下面的方式重写循环时
for (std::size_t i = 0, sz = array_wrapper.size; i < sz; ++i)
process_value(array_wrapper.arr[i]);
Run Code Online (Sandbox Code Playgroud)
,每次迭代结束时不必要的读取都将消失.但是初始写入[rsp + 512]将保留,这意味着编译器仍然希望我们能够array_wrapper.size从这些process_value调用中访问该位置的变量(通过做一些奇怪的基于偏移的魔术).
为什么?
这是现代编译器实现中的一个小缺点(希望很快就会修复)?或者,当我们将数组及其大小放入同一个类时,C++标准是否确实需要这样的行为导致生成效率较低的代码?
PS
我意识到上面的代码示例可能看起来有点人为.但请考虑一下:我想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];
}
};
Run Code Online (Sandbox Code Playgroud)
用法:
void test_pod_vector(std::size_t size)
{
PODVector<double, max_size> arr(size);
for (double& val : arr)
process_value(val);
}
Run Code Online (Sandbox Code Playgroud)
如果上面描述的问题确实是由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++的Emergent Structures中,他解释了为什么优化器在调用接受引用/指针参数的函数时会遇到困难.仅在绝对必要时才使用引用/指针函数参数.
如果您想更改该值,则性能更高的选项是:
double process_value(double value);
Run Code Online (Sandbox Code Playgroud)
然后:
array_wrapper.arr[i] = process_value(array_wrapper.arr[i]);
Run Code Online (Sandbox Code Playgroud)
此更改会导致最佳装配:
.L23:
movsd xmm0, QWORD PTR [rbx]
add rbx, 8
call process_value2(double)
movsd QWORD PTR [rbx-8], xmm0
cmp rbx, rbp
jne .L23
Run Code Online (Sandbox Code Playgroud)
要么:
for(double& val : arr)
val = process_value(val);
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
302 次 |
| 最近记录: |