为什么 GCC 会为 `std::ranges::max` 中的每次比较复制对象?

问题描述 投票:0回答:1

考虑以下示例(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
的原因吗?还是一个 bug?

c++ gcc stl clang std-ranges
1个回答
1
投票

我认为这是 gcc 实现中的一个“bug”。

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::
,以便能够在标准库实现内部的自然栖息地之外使用算法。

© www.soinside.com 2019 - 2024. All rights reserved.