【发布时间】:2019-01-11 14:26:47
【问题描述】:
我想用std::array来存储N维向量的数据,并对这些向量进行算术运算。我想,由于 std::array 现在有一个 constexpr size() 成员函数,我可以使用它来展开我需要的循环,以便对其元素进行算术运算。
这是一个最小的例子:
#include <array>
#include <type_traits>
#include <iostream>
#include <cassert>
template<std::size_t N=0, typename Vector>
void plus_equals(Vector& result, Vector const& input)
{
result[N] += input[N];
if constexpr (N + 1 < result.size())
plus_equals<N+1>(result, input);
}
template<typename INT, size_t N>
class Vector
{
std::array<INT, N> data_;
public:
template<typename ... BracketList>
Vector(BracketList ... blist)
:
data_{std::forward<BracketList>(blist)...}
{}
INT& operator[](std::size_t i)
{
return data_[i];
}
INT operator[](std::size_t i) const
{
return data_[i];
}
decltype(auto) begin() const
{
return data_.begin();
}
decltype(auto) end() const
{
return data_.end();
}
decltype(auto) end()
{
return data_.end();
}
constexpr decltype(auto) size()
{
return data_.size();
}
void operator+=(Vector const& other)
{
plus_equals(*this, other);
}
};
template<size_t N = 0, typename Vector>
Vector operator+(Vector const& uVec, Vector const& vVec)
{
Vector result {uVec};
result += vVec;
return result;
}
template<size_t N = 0, typename Vector>
Vector sum(Vector const& uVec, Vector const& vVec)
{
Vector result {uVec};
for (decltype(result.size()) i = 0; i < result.size(); ++i)
result[i] += vVec[i];
return result;
}
template<typename Vector>
void print(Vector&& v)
{
for (const auto& el : v) std::cout << el << " ";
std::cout << std::endl;
}
using namespace std;
int main()
{
Vector<int, 3> c1 = {1,2,3};
Vector<int, 3> c2 = {3,2,1};
auto r1 = c1 + c2;
print (r1);
auto r2 = sum(c2, c2);
print (r2);
Vector<int, 3> s1, s2;
for (std::size_t i = 0; i < 3; ++i)
cin >> s1[i];
for (std::size_t i = 0; i < 3; ++i)
cin >> s2[i];
auto r3 = s1 + s2;
print(r3);
auto r4 = sum(s1, s2);
print(r4);
return 0;
}
sum 操作是使用 plus_equals 实现的,它应该对 Vector 的元素展开单独的 += 操作,而 sum(Vector const&, Vector const&) 函数使用 for 循环。
我使用-O3 -std=c++2a 在godbolt 上编译了示例。
如果我注释掉除了
之外的所有内容Vector<int, 3> c1 = {2,11,7};
Vector<int, 3> c2 = {9,22,5};
auto r1 = c1 + c2;
print (r1);
我明白了
movabs rax, 141733920779
sub rsp, 24
lea rdi, [rsp+4]
mov QWORD PTR [rsp+4], rax
mov DWORD PTR [rsp+12], 12
call void print<Vector<int, 3ul>&>(Vector<int, 3ul>&)
xor eax, eax
add rsp, 24
ret
这里发生了什么?为什么我看不到前两个常量c1[0] + c2[0] 和c1[1] + c2[1]?另一方面7 + 5 = 12 被移动:
mov DWORD PTR [rsp+12], 12
为什么是汇编代码
int main()
{
Vector<int, 3> c1 = {2,11,7};
Vector<int, 3> c2 = {9,22,5};
//auto r1 = c1 + c2;
//print (r1);
auto r2 = sum(c1, c2);
print (r2);
完全一样吗?
如果我尝试使用运行时变量:
Vector<int, 3> s1, s2;
for (std::size_t i = 0; i < 3; ++i)
cin >> s1[i];
for (std::size_t i = 0; i < 3; ++i)
cin >> s2[i];
auto r3 = s1 + s2;
print(r3);
我明白了
mov edx, DWORD PTR [rsp+28]
mov eax, DWORD PTR [rsp+32]
lea rdi, [rsp+36]
add eax, DWORD PTR [rsp+20]
add edx, DWORD PTR [rsp+16]
mov ecx, DWORD PTR [rsp+24]
add ecx, DWORD PTR [rsp+12]
mov DWORD PTR [rsp+44], eax
mov DWORD PTR [rsp+36], ecx
mov DWORD PTR [rsp+40], edx
链接到plus_equals 函数模板并按预期展开迭代。
对于sum:
Vector<int, 3> s1, s2;
for (std::size_t i = 0; i < 3; ++i)
cin >> s1[i];
for (std::size_t i = 0; i < 3; ++i)
cin >> s2[i];
//auto r3 = s1 + s2;
//print(r3);
auto r4 = sum(s1, s2);
print(r4);
程序集是:
mov edx, DWORD PTR [rsp+32]
add edx, DWORD PTR [rsp+20]
add ecx, eax
shr rax, 32
add eax, DWORD PTR [rsp+28]
mov DWORD PTR [rsp+44], edx
mov DWORD PTR [rsp+40], eax
mov DWORD PTR [rsp+36], ecx
并且没有相等比较和跳转,所以循环已经展开。
当我查看sum模板的汇编代码时,那里有比较运算符和跳转。这是我所预料的,因为我认为编译器首先为任何Vector 生成通用代码,然后再确定Vector::size() 是否为constexpr 并应用进一步的优化。
翻译可以吗?如果是这样,是否可以得出结论,对于固定大小的数组手动展开迭代没有意义,因为使用-O3,使用constexpr size 成员函数的循环无论如何都会被编译器展开?
【问题讨论】:
-
只是为了确定,您是否测量并分析了您之前的代码是否会因为您的要求而变慢,或者它是您程序中的瓶颈?您是否检查过编译器是否还没有进行一些展开或其他优化?这不是过早的优化吗?
-
我已经使用 hpctoolkit 和 scorep 对我的代码进行了多次分析。这与我的问题没有任何关系:如果可以保证编译器在循环定义中有
constexpr size()时会展开循环,那么使用 for 循环和使用递归函数模板会使代码更具可读性.这很重要,因为代码是科学的,可能会被其他人扩展。 -
标准不保证任何循环都会展开。事实上,编译器甚至可能重新滚动手动展开的循环(我不知道编译器会这样做的情况 - 但这种情况可能存在)。
-
" 如果有保证编译器" - 你甚至不必继续,答案已经是'否',所以我想这回答了你的问题。无法保证编译器如何在语言规范之外进行任何优化。
-
如果您希望 gcc/clang 在编译时已知大小时展开循环(无论
constexpr与否),请使用标志-funroll-loops。--param max-unroll-times=...可以控制展开深度。如今,编译器通常不会进行积极的展开,因为 CPU 可以非常有效地处理循环。但是由于jcc对吞吐量的影响不可忽略,在某些情况下添加-funroll-all-loop会带来显着的改善。尤其是一些低于标准的 gcc 矢量化,它们具有与展开循环相同的性能,默认情况下不这样做。
标签: c++ optimization stdarray loop-unrolling