与min_element和max_element一起使用相比,使用minmax_element是否有效率上的优势?

Is there any efficiency advantage in using minmax_element over min_element and max_element together?

本文关键字:element 有效率 是否 minmax 使用 max min 一起      更新时间:2023-10-16

std::minmax_element:返回一个数组对,其中指向最小元素的迭代器作为第一个元素,指向最大元素的迭代器作为第二个元素。

std::min_element:返回指向[first, last)范围内最小元素的迭代器。

std::max_element:返回指向[first, last)范围内最大元素的迭代器。


std::minmax_element是否使用完整列表的排序来实现这一点?

std::minmax_element处理返回对的开销是否足够值得?

您不必担心std::minmax_element进行任何排序。它以它被遍历的方式离开范围。它更有效的原因是它可以在一次遍历中找到最大值和最小值,而当分别寻找最大值和最小值时,你必须做两次完整的遍历。

std::minmax_element具有max(floor(3/2(N−1)), 0)的复杂性,而std::max_elementstd::min_element都是max(N-1,0),因此使用std::minmax_element可以减少约25%的操作。

std::minmax_element找到最后一个最大的元素,而std::max_element找到第一个最大的元素。

所以如果你需要找到一个范围的最小值和最大值,那么你应该使用std::minmax_element。如果你只需要最小值或最大值,那么你应该使用专门的版本。在即将到来的c++ 17标准和结构化绑定中,处理std::minmax_element的返回将变得更加容易。你可以写

auto [min, max] = std::minmax_element(...);

现在第一个元素存储在min中,第二个元素存储在max中。

其他答案都很好。我想添加一些关于minmax_element的工作原理,但是,这也有助于解释为什么它(通常)比单独运行min_elementmax_element更好,并讨论一些特定情况下,执行得更好。

如果我们考虑一个简单的实现,您将维护最大值和最小值(以及它们对应的迭代器),并简单地遍历范围,将每个值与最小值和最大值进行比较,并在必要时进行调整。然而,这将给你总共2N个比较;虽然它可能比遍历列表两次执行得更好(由于更好地使用了局部性),但该规范要求(大约)进行3/2 N次比较。那怎么可能呢?

它通过处理成对而不是单独的项来工作。取范围内的前两项(#0和#1),我们可以比较它们,并将最大的分配给max-value,最小的分配给min-value。然后,我们比较接下来的两个项目(#3和#4),以确定其中哪个更大;我们将较大的值与max-value进行比较,将较小的值与min-value进行比较,并根据需要更新max-value/min-value。然后我们对每一对(#5和#6,然后#7和#8,等等)重复这个过程。

所以,每对需要三次比较——相互比较,然后最高的与当前的最大值比较,最低的与当前的最小值比较。这将需要的比较次数减少到3/2n !

根据下面的评论,然而,应该注意的是,当使用比较便宜的类型(或比较器)时,这种"改进的"算法往往会在现代处理器上产生比原始版本更差的性能-特别是,在vector<int>或类似的范围内:每对元素之间的比较具有不可预测的结果,导致处理器中的分支预测失败(尽管这只在数据或多或少随机排序时才会发生);当前的编译器并不总是将分支转换为条件传输,尽管它们可能这样做。此外,编译器更难对更复杂的算法进行矢量化。

理论上,我认为c++库实现可以为minmax_element函数提供重载,该函数使用原始(int等)元素类型的朴素算法和默认比较器。虽然标准规定了比较次数的限制,但如果不能观察到这些比较的效果,那么实际计算的次数就不重要了,只要时间复杂度相同(在两种情况下都是- O(N))。然而,尽管对于随机排序的数据,这可能会提供更好的性能,但对于有序的数据,这可能会产生更差的性能。

考虑到以上所有内容,一个简单的测试用例(如下)显示了一个有趣的结果:对于随机排序的数据,分别使用min_elementmax_element实际上比使用minmax_element稍微快一些。然而,对于排序数据,minmax_element比单独使用min_elementmax_element要快得多。在我的系统(Haswell处理器)上(用gcc -O3 -std=c++11 -march=native编译,GCC版本5.4),一个示例运行显示min/max分别为692毫秒,minmax组合为848毫秒。当然,每次运行之间会有一些差异,但这些值似乎是典型的。

注意:

  • 性能差异很小,不太可能成为实际程序中的主导因素;
  • 差异取决于编译器优化;在未来,结果很可能会逆转;
  • 对于更复杂的数据类型(或者更确切地说,对于更复杂的比较器),结果可能会相反,因为在这种情况下,较少的比较可能是显着的改进。
  • 当样本数据是有序的而不是随机的(用v.push_back(i)替换v.push_back(r(gen))在下文中的程序中),性能是非常不同:对于单独的min/max,它大约是728毫秒,而对于组合的minmax,它下降到246毫秒。

代码:

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
constexpr int numEls = 100000000;
void recresult(std::vector<int> *v, int min, int max)
{
   // Make sure the compiler doesn't optimize out the values: 
   __asm__ volatile (
       ""
       :
       : "rm"(v), "rm"(min), "rm"(max)
   );
}
int main(int argc, char **argv)
{
    using namespace std;
    std::mt19937 gen(0);
    uniform_int_distribution<> r(0, 100000);

    vector<int> v;
    for (int i = 0; i < numEls; i++) {
        v.push_back(r(gen));
    }
    // run once for warmup
    int min = *min_element(v.begin(), v.end());
    int max = *max_element(v.begin(), v.end());
    recresult(&v, min, max);
    // min/max separately:
    {
        auto starttime = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < 5; i++) {
        int min = *min_element(v.begin(), v.end());
            int max = *max_element(v.begin(), v.end());
            recresult(&v, min, max);
        }
        auto endtime = std::chrono::high_resolution_clock::now();
        auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(endtime - starttime).count();
        cout << "min/max element: " << millis << " milliseconds." << endl;
    }
    // run once for warmup
    auto minmaxi = minmax_element(v.begin(), v.end());
    recresult(&v, *(minmaxi.first), *(minmaxi.second));
    // minmax together:
    {
        auto starttime = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < 5; i++) {
        minmaxi = minmax_element(v.begin(), v.end());
        recresult(&v, *(minmaxi.first), *(minmaxi.second));
        }
        auto endtime = std::chrono::high_resolution_clock::now();
        auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(endtime - starttime).count();
        cout << "minmax element: " << millis << " milliseconds." << endl;
    }
    return 0;
}

是。你只迭代一次范围,而不是做两次。

std::minmax_element复杂度:

最大max(floor(3/2(N−1)),0)个应用,其中N = std::distance(first, last)

std::min_element复杂度(与max_element相同):

精确的max(N-1,0)比较,其中N = std::distance(first, last)

忽略maxfloor,我们得到:
(N-1) * 2 vs 3/2 (N-1)

所以通过使用minmax_element,你得到3/4的比较,你需要使用max_element + min_element,或更好。

minmax_element使用了<运算符的传递性,它知道如果它在更新最小值,它不需要通过一次比较两个元素来比较最大值,即如果a < b,我们只需要检查min(a, current_min)max(b, current_max),反之亦然。

值得注意的是:

该算法与std::make_pair(std::min_element(), std::max_element())的区别不仅在于效率,还在于该算法查找最后一个最大元素,而std::max_element查找第一个最大元素。