将minmax_element与min_element和max_element一起使用是否有效率优势?

Sau*_*ahu 5 c++ max min

std::minmax_element :返回一对由迭代器组成的对,作为第一个元素的最小元素和作为第二个元素的最大元素的迭代器.

std::min_element :将迭代器返回到[first,last]范围内的最小元素.

std::max_element :将迭代器返回到[first,last]范围内的最大元素.


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

被处理的开销恢复对来自std::minmax_element足够值得?

Nat*_*ica 13

您不必担心std::minmax_element进行任何排序.它以精确的方式离开范围.它更有效的原因是它可以在单个通道中找到最大值和最小值,而当分别查找最大值和最小值时,您必须执行两次完整遍历.

std::minmax_element具有复杂性的max(floor(3/2(N?1)), 0)地方std::max_elementstd::min_element每个都是max(N-1,0)如此,它使用的操作减少了约25%std::minmax_element

std::minmax_element找到最大的元素同时std::max_element找到第一个最大的元素也有区别.

因此,如果您需要找到范围的最小值和最大值,那么您应该使用std::minmax_element.如果您只需要最小值或最大值,则应使用专用版本.使用std::minmax_element即将推出的C++ 17标准和结构化绑定,处理返回将变得更加容易.你将能够写作

auto [min, max] = std::minmax_element(...);
Run Code Online (Sandbox Code Playgroud)

现在该对的第一个元素存储在其中min,第二个元素存储在其中max.


dav*_*mac 6

其他答案都很好.我想补充一点关于minmax_element必然如何工作,但这也有助于解释为什么它(通常)比运行min_elementmax_element单独工作更好,并讨论一些不能更好的特定情况.

如果我们考虑一个简单的实现,你将保持最大值和最小值(及其相应的迭代器)并简单地遍历范围,将每个值与最小值和最大值进行比较并根据需要进行调整.但是,这将为您提供总共2N的比较; 虽然它可能比在列表中迭代两次(由于更好地使用局部性)更好地执行,但规范要求(大致)3/2 N比较.那怎么可能呢?

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

因此,每对需要进行三次比较 - 相互之间,然后是当前最大值的最高值,最低值是当前最小值.这减少了3/2 N所需的比较次数!

然而,根据下面的评论,应该注意的是,当使用比较便宜的类型(或比较器)时,这种"改进的"算法在现代处理器上的性能往往比天真版本更差 - 特别是在范围超过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=nativeGCC版本5.4 编译),样品运行分别显示692毫秒的最小/最大值和848的最小值组合.当然,运行之间存在一些差异,但这些值似乎很典型.

注意:

  • 性能差异很小,不太可能成为现实世界计划的主导因素;
  • 差异取决于编译器优化; 在未来,结果可能会逆转;
  • 对于更复杂的数据类型(或者更确切地说是对于更复杂的比较器),结果可能会逆转,因为在这种情况下,较少的比较可能是显着的改进.
  • 当样本数据是有序的,而不是随机的(取代v.push_back(r(gen))v.push_back(i)在下面的程序)的性能是非常不同:对单独最小/最大,它的周围728毫秒,而对于组合极大极小,它下降到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;
}
Run Code Online (Sandbox Code Playgroud)

  • 有趣的事实:在现代处理器上,对于元素为随机顺序的向量&lt;int&gt;,由于分支预测,这些 3N/2 比较比进行简单的 2N 比较花费的时间要长得多。 (2认同)

krz*_*zaq 5

是.你只需要在范围内迭代一次而不是两次.