为什么 GCC 会复制“std::ranges::max”中每个比较的对象?

Why does GCC copy object for each comparison in `std::ranges::max`?

提问人:Andrew 提问时间:11/2/2023 最后编辑:Peter MortensenAndrew 更新时间:11/4/2023 访问量:714

问:

请看以下示例 (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

C++ gcc stl clang std-ranges

评论

0赞 Sam Varshavchik 11/2/2023
我的下一步是在复制构造函数中设置一个断点,然后尝试找出它在模板中的位置被调用,以及为什么。
0赞 Christian Stieber 11/2/2023
好吧,你问的是“为什么”,我不能给出——我不是一个 STL 巫师,不会立即想出一个解释。但是,我可以想出一个解决方案:std::cout << std::ranges::max_element( vec, [] ( const auto& a, const auto& b ) { std::cout << "cmp" << std::endl; return a.x < b.x; } )->x;
0赞 Richard Critten 11/2/2023
尝试制作 move 构造函数。标准容器在移动东西时需要安全。noexcept
0赞 Pete Becker 11/2/2023
这并不能解决问题,但此代码中的任何内容都不需要额外的内容。 结束一行。std::endl'\n'
0赞 Andrew 11/2/2023
@RichardCritten 考虑到您的意见,我更新了问题。它仍然产生相同的输出。

答:

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_iteratorauto&&

[我] ...或者只是供应给*__firststd::__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 我现在已经写了一个错误报告。让我们看看结果如何。