【问题标题】:Passing by reference to comparator function (C++11)通过引用传递给比较器函数 (C++11)
【发布时间】:2021-11-04 08:24:59
【问题描述】:

我正在尝试加快我的代码速度(下面是最小的、可重现的示例),有人告诉我,通过引用传递对于我的比较器函数来说是一种更有效的方法。那是我第一次听说这个短语,所以我查了一下,找到了一些带有示例的网站,但我不明白何时以及如何使用它。在这种情况下我将如何使用它?

#include <array>
#include <iostream>
#include <algorithm>
#include <fstream>
#include <ctime>
#include <random>

using namespace std;

class arrMember {
public:
    int var;
    arrMember(int var) :
        var(var) {}
    arrMember() {};
};

array<int, 1000000> arraySource;

array<arrMember, 1000000> arrayObjects;

bool compare(arrMember(x), arrMember(y)) {
    return (x.var < y.var);
}

void arrayPrint() {
    ofstream output("arrayPrint.txt");
    for (int k = 0; k != arrayObjects.size(); k++) {
        output << arrayObjects[k].var << endl;
    }

    output.close();
}

void sort() {
    int j = 0;
    for (auto i = arraySource.begin(); i != arraySource.end(); i++, j++) {
        arrayObjects[j] = arrMember(arraySource[j]);
    }

    sort(arrayObjects.begin(), arrayObjects.end(), compare);
}

int main(){
    random_device rd{};
    mt19937 engine{ rd() };
    uniform_int_distribution<int> dist{ 0, 9999 };
    for (int x = 0; x < arraySource.size(); ++x){
        arraySource[x] = dist(engine);
    }

    cout << "Executing sort...\n";
    clock_t t1 = clock();
    sort();
    clock_t t2 = clock();
    double timeDiff = ((double)(t2 - t1)) / CLOCKS_PER_SEC;

    cout << "Sort finished. CPU time was " << timeDiff << " seconds.\n";

    arrayPrint();

    return 0;
}

感谢您的帮助。

【问题讨论】:

    标签: c++ sorting c++11 optimization pass-by-reference


    【解决方案1】:

    对于非常小的类型,通过引用传递没有帮助;复制构造一个由单个 int 组成的类与获取现有实例的地址的成本基本相同,复制构造意味着比较器不需要取消引用指针来查找值,它已经在本地堆栈。

    对于具有昂贵复制构造的较大类型,您可以更改(原始代码减去不必要的括号):

    bool compare(arrMember x, arrMember y) {
        return x.var < y.var;
    }
    

    到:

    bool compare(const arrMember& x, const arrMember& y) {
        return x.var < y.var;
    }
    

    并获得有意义的加速,但对于一个简单的 int 包装类,您将一无所获。

    无论class 的大小如何, 有所不同的是用仿函数(或 lambda,它是仿函数的语法糖)替换原始函数。 std::sort 将模板专门用于比较器的 type,并且函数本身不是类型;它们实际上是共享相同原型的一组函数的实例。所以如果你同时实现:

    bool compare(const arrMember& x, const arrMember& y) {
        return x.var < y.var;
    }
    bool compareReverse(const arrMember& x, const arrMember& y) {
        return x.var > y.var;
    }
    

    并在程序中的不同点同时使用comparecompareReverse 调用std::sort,它只会为bool (*)(const arrMember&amp;, const arrMember&amp;) 生成std::sort 的一种特化,并且该单一特化必须实际传递并调用提供的通过指针函数;调用函数的成本明显高于执行比较本身的微不足道的成本,通过指针调用通常更昂贵。

    相比之下,仿函数(和 lambda)是独特的类型,因此std::sort 可以完全专门化它们,包括内联比较,因此不会调用比较器函数;比较器逻辑直接内联到 std::sort 的独特特化中。所以而不是:

    bool compare(const arrMember& x, const arrMember& y) {
        return x.var < y.var;
    }
    std::sort(..., compare);
    

    你可以这样做:

    struct compare {
        bool operator()(const arrMember& x, const arrMember& y) const {
            return x.var < y.var;
        }
    };
    std::sort(..., compare());
    

    或将整个内容内联为 C++11 lambda:

    std::sort(..., [](const arrMember& x, const arrMember& y) { return x.var < y.var; });
    

    无论哪种方式,代码都会运行得更快; Godbolt shows 两种函数指针方法几乎相同,而使用仿函数方法,相对于函数指针方法,您将运行时间减少了大约三分之一(在这种情况下节省了更多的值传递,但几乎不值得麻烦大部分时间都在考虑;我默认通过 const 引用传递,并且仅在分析表明它很慢并且类型足够简单以至于按值传递可能会有所帮助时才考虑切换。

    模板和仿函数的这个好处就是为什么 C++ 的 std::sort 在适当使用时可靠地击败 C 的 qsort; C 缺少模板,因此它根本无法专门化qsort,并且必须始终通过函数指针调用比较器。如果您将std::sort 与函数一起使用,它对qsort 并没有真正的改进,但与仿函数/ lambda 一起使用,它会生成快得多 的代码,但会产生更多 em> 代码(std::sort 对每个仿函数/lambda 的独特特化)。您可以通过复制粘贴实现qsort 的代码、摆脱比较器并自己内联比较来在C 中获得相同的好处,但这是很多 的维护开销; C++ 模板为您完成了 99% 的工作,您只需要记住在回调中使用函子/lambda,而不是原始函数。

    【讨论】:

    • 非常有趣。稍后我将尝试使用 lambda 函数。感谢您的回复。如果可以的话,我会赞成你的回答。
    • @ShadowRanger 没有什么可以阻止编译器为编译时已知的一组参数生成函数的专用(部分评估)版本。似乎没有编译器可以这样做(至少对于 C++,但 ghc (Haskell) 可以),但同样应该通过内联来实现。 std::sort 可以内联,然后对谓词的调用也可以内联。但是,std::sort 似乎足够复杂,不能被内联。如果你例如,它会完全改变。使用 std::find_if。在这里,带有函数的调用被内联但不是仿函数:godbolt.org/z/xhMxHq
    【解决方案2】:

    在您的代码中,您将比较器函数定义为

    bool compare(arrMember(x), arrMember(y)) {
        return (x.var < y.var);
    }
    

    该函数有两个arrMember类型的参数,按值传递。这意味着xy 是传递给函数的参数的副本。因此,当您对数组进行排序时,每次调用比较器(O(n * logn) 次)时,都会创建、比较然后销毁两个临时对象。您可以修改函数以通过引用获取参数:

    bool compare(arrMember const& x, arrMember const& y) {
        return x.var < y.var;
    }
    

    这样,函数不会使用对原始值的复制引用。这是一个无法修改的 const 引用。

    现在的想法是,这会将临时对象保存为副本,从而节省运行时间。然而,最好的建议是衡量而不是争论。我已经稍微修改了您的代码以运行这两个版本。

    #include <array>
    #include <iostream>
    #include <algorithm>
    #include <fstream>
    #include <ctime>
    #include <random>
    
    using namespace std;
    
    constexpr size_t N=1000000;
    
    class arrMember {
    public:
        int var;
        arrMember(int var) :
            var(var) {}
        arrMember() {};
    };
    
    bool compare_by_value(arrMember x, arrMember y) {
        return (x.var < y.var);
    }
    
    bool compare_by_reference(arrMember const& x, arrMember const& y) {
        return (x.var < y.var);
    }
    
    
    template<typename C>
    void sort(array<arrMember, N>& a, C comp) {
        sort(a.begin(), a.end(), comp);
    }
    
    int main(){
        random_device rd{};
        mt19937 engine{ rd() };
        uniform_int_distribution<int> dist{ 0, 9999 };
    
        array<arrMember, N> a;
        std::generate(a.begin(), a.end(), [&]() {return arrMember{dist(engine)};});
    
        int tmp=0;
        cout << "Executing sort...\n";
        {
            array<arrMember, N> x = a;
            clock_t t1 = clock();
            sort(x, compare_by_value);
            clock_t t2 = clock();
            double timeDiff = ((double)(t2 - t1)) / CLOCKS_PER_SEC;
            cout << "Sort finished. CPU time was " << timeDiff << " seconds.\n";
            tmp += x.front().var;
        }
    
        {
            array<arrMember, N> x = a;
            clock_t t1 = clock();
            sort(x, compare_by_reference);
            clock_t t2 = clock();
            double timeDiff = ((double)(t2 - t1)) / CLOCKS_PER_SEC;
            cout << "Sort finished. CPU time was " << timeDiff << " seconds.\n";
            tmp += x.front().var;
        }
    
    
    
        return tmp;
    }
    

    Here 是输出示例:

    开始

    正在执行排序... 排序完毕。 CPU 时间为 0.105382 秒。 排序完毕。 CPU 时间为 0.108179 秒。

    0

    完成

    似乎两个版本之间没有区别,那么发生了什么?优化编译器会将比较器内联到排序算法代码中。为此,排序路由中的调用将替换为函数的主体,并且由于函数不修改参数,因此不必创建副本以防止修改“传递”的值。

    【讨论】:

    • 感谢您的回答,这很有意义。但现在我又回到了绘图板上,以了解如何更快地排序......
    • @jared std::sort 是 O(n logn),这是一般排序算法的最佳复杂度。根据您的输入数据,您可以更快地找到一些东西,例如当数据大部分被排序时,另一种排序算法可能会执行得更好,但不是在最坏的情况下。如果您想对整数进行排序,Bucketsort 可能是一种选择。
    • @jared: 另请注意:您的原始基准(但不是 Jens 的)包括从源数组复制到临时数组的成本(而且它只在新内存上完成一次,在 Linux 上 -类似的系统可能会延迟写入时复制映射到零页,从而增加了初始化它的成本)。所以你测量的一些成本不是那种,它是分配和初始化 4 MB 内存的成本。因此,请确保将基准限制在排序本身。之后,是的,在这种情况下计数排序会表现得更好(以相对较小的 10000 个元素数组的计数成本)。
    • 旁注:你确定编译器内联了比较吗?使用函数而不是仿函数(或 lambda,它只是仿函数的语法糖)应该会抑制这种优化(因为 sort 的模板只能专门针对函数原型,而不是特定函数)。似乎通过引用传递无关紧要,因为所讨论的类实际上是一种 POD 类型,复制的成本与引用它的成本一样低。
    • 在测试中,使用适当的函数进行内联,functors are consistently faster, but functors receiving by reference are actually slower than functors receiving by value(这是有道理的,考虑到复制相关对象的成本是多么低)。
    猜你喜欢
    • 1970-01-01
    • 2017-08-12
    • 1970-01-01
    • 1970-01-01
    • 2011-12-07
    • 2015-09-19
    • 2014-12-03
    • 2023-03-08
    相关资源
    最近更新 更多