提问人:Andrew 提问时间:11/2/2023 最后编辑:Peter MortensenAndrew 更新时间:11/4/2023 访问量:714
为什么 GCC 会复制“std::ranges::max”中每个比较的对象?
Why does GCC copy object for each comparison in `std::ranges::max`?
问:
请看以下示例 (Godbolt):
#include <vector>
#include <iostream>
#include <ranges>
#include <algorithm>
struct A
{
A() {}
A( const A& ) { std::cout << "Copy\n"; }
A( A&& ) noexcept { std::cout << "Move\n"; }
A& operator=(const A&) { std::cout << "Copy assigned\n"; return *this; }
A& operator=( A&& ) noexcept { std::cout << "Move assigned\n"; return *this; }
int x = 10;
};
int main()
{
std::vector<A> vec( 10 );
std::cout << "Init\n";
std::cout << std::ranges::max( vec, [] ( const auto& a, const auto& b ) { std::cout << "cmp" << std::endl; return a.x < b.x; } ).x;
}
这个用 GCC 13.2 编译的程序(即使打开了 -O3
优化)会产生以下输出:
Init
Copy
Copy
cmp
Copy
cmp
Copy
cmp
Copy
cmp
Copy
cmp
Copy
cmp
Copy
cmp
Copy
cmp
Copy
cmp
10
但是使用 Clang 17(具有和任何优化级别)编译,它根本不执行复制(据我所知,返回的值除外):-stdlib=libc++
Init
cmp
cmp
cmp
cmp
cmp
cmp
cmp
cmp
cmp
Copy
10
如果具有成本高昂的复制构造函数,则这种差异将大大降低性能。A
GCC 有这个实现是有原因的还是一个错误?std::ranges::max
答:
16赞
Ted Lyngmo
11/2/2023
#1
我认为这是 gcc 实现中的一个“错误”,我写了一份错误报告。
LLVM 在正在使用的重载中有两个版本:operator()
auto __first = std::ranges::begin(__r);
auto __last = std::ranges::end(__r);
_LIBCPP_ASSERT(__first != __last, "range must contain at least one element");
if constexpr (std::ranges::forward_range<_Rp>) {
// MY COMMENT: This is what's actually being used:
auto __comp_lhs_rhs_swapped = [&](auto&& __lhs, auto&& __rhs) {
return std::invoke(__comp, __rhs, __lhs);
};
return *std::ranges::min_element(std::move(__first),
std::move(__last),
__comp_lhs_rhs_swapped, __proj);
} else {
std::ranges::range_value_t<_Rp> __result = *__first;
while (++__first != __last) {
if (std::invoke(__comp, std::invoke(__proj, __result),
std::invoke(__proj, *__first)))
__result = *__first;
}
return __result;
}
..但是,如果我禁用当前正在使用的版本并改用循环,这并不重要。它仍然没有复制。while
现在对于 GCC 案例中的重载:operator()
auto __first = std::ranges::begin(__r);
auto __last = std::ranges::end(__r);
__glibcxx_assert(__first != __last);
auto __result = *__first;
while (++__first != __last) {
auto __tmp = *__first;
if (std::__invoke(__comp, std::__invoke(__proj, __result),
std::__invoke(__proj, __tmp)))
__result = std::move(__tmp);
}
return __result;
副本在这里:
auto __tmp = *__first;
我认为它应该是:
auto& __tmp = *__first;
因为有了这个变化,它就不再复制了。
注意:我已经在几个地方添加了 和,以便能够在其自然栖息地之外使用算法,即标准库实现内部。std::
std::ranges::
更新
该错误现已得到确认。乔纳森·韦克利(Jonathan Wakely)也回答说:
[我]
auto& __tmp = *__first;
[JW]这不会编译为或代理引用。我想没关系。
move_iterator
auto&&
[我] ...或者只是供应给
*__first
std::__invoke
[JW]我认为这也没关系。
因此,如果他的初步评估是正确的,那么对于某人来说,这应该是一个唾手可得的果实,我们可以希望在不久的发布中得到解决。
评论
0赞
Andrew
11/2/2023
auto& __tmp = *__first;
确实不会复制,但我认为不需要迭代器来返回可引用的值。它可能会返回一个临时的。我认为在 LLVM 中正是这一点。std::ranges::range_value_t<_Rp>
0赞
Ted Lyngmo
11/2/2023
@Andrew这也是我的第一反应,但只是让它没有效果。我没有在那里深入挖掘。std::ranges::range_value_t<_Range> __tmp = *__first;
3赞
Ted Lyngmo
11/2/2023
@Andrew 我现在已经写了一个错误报告。让我们看看结果如何。
评论
std::cout << std::ranges::max_element( vec, [] ( const auto& a, const auto& b ) { std::cout << "cmp" << std::endl; return a.x < b.x; } )->x;
noexcept
std::endl
'\n'