【问题标题】:Sum over vector performing differently for nearly identical code对几乎相同的代码执行不同的向量求和
【发布时间】:2020-05-14 23:14:21
【问题描述】:

当使用g++ 标志-O3 编译时,这些函数对数组和向量求和似乎存在性能差异:

float sum1(float* v, int length) {
    float sum = 0;
    for(int i = 0; i < length; i++) {
        sum += v[i];
    }
    return sum;
}

float sum2(std::vector<float> v) {
    return sum1(&v[0], v.size());
}

当调用 sum1 时,例如一个长度为 100000 和 sum2 的向量具有相同的长度和内容,sum2 最终约为。在我的测试中比 sum1 慢 10%。 测量的运行时间是:

sum1: 0.279816 ms
sum2: 0.307811 ms

现在这个开销是从哪里来的?附加您还可以找到我在那里犯错的可能性的完整测试代码。

[更新] 当通过引用 (float sum2(std::vector&lt;float&gt;&amp; v)) 调用时,性能差异约为还剩 3.7%,所以这有帮助,但其他地方仍然有一些性能损失?

[Update2] 其余部分似乎在统计上占主导地位,如更多迭代所示。因此,唯一的问题是通过引用调用!


完整的测试代码(用g++标记-O3编译,也用clang++测试):

#include <iostream>
#include <chrono>
#include <vector>

using namespace std;

std::vector<float> fill_vector(int length) {
    std::vector<float> ret;

    for(int i = 0; i < length; i++) {
        float r = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
        ret.push_back(r);
    }

    return ret;
}

float sum1(float* v, int length) {
    float sum = 0;
    for(int i = 0; i < length; i++) {
        sum += v[i];
    }
    return sum;
}

float sum2(std::vector<float> v) {
    return sum1(&v[0], v.size());
}

int main() {
    int iterations = 10000;
    int vector_size = 100000;

    srand(42);
    std::vector<float> v1 = fill_vector(vector_size);

    float* v2;
    v2 = &v1[0];

    std::chrono::duration<double, std::milli> duration_sum1(0);
    for(int i = 0; i < iterations; i++) {
        auto t1 = std::chrono::high_resolution_clock::now();
        float res = sum1(v2, vector_size);
        auto t2 = std::chrono::high_resolution_clock::now();
        cout << "Result sum1: " << res << endl;
        duration_sum1 += t2 - t1;
    }
    duration_sum1 /= iterations;

    std::chrono::duration<double, std::milli> duration_sum2(0);
    for(int i = 0; i < iterations; i++) {
        auto t1 = std::chrono::high_resolution_clock::now();
        float res = sum2(v1);
        auto t2 = std::chrono::high_resolution_clock::now();
        cout << "Result sum2: " << res << endl;
        duration_sum2 += t2 - t1;
    }
    duration_sum2 /= iterations;

    cout << "Durations:" << endl;
    cout << "sum1: " << duration_sum1.count() << " ms" << endl;
    cout << "sum2: " << duration_sum2.count() << " ms" << endl;
}

【问题讨论】:

  • 尝试改变测试的顺序...在 sum1 之前调用 sum2
  • 尝试更改float sum2(std::vector&lt;float&gt;&amp; v)。
  • @2xB float sum2(std::vector&lt;float&gt; v) -- C++ 有多种传递参数的方式。不幸的是,您选择了“按价值”,而不是“按参考”。
  • @PetokLorand 按值传递调用向量的深层副本。
  • 除此之外:更喜欢v.data() 而不是&amp;v[0]。还有float sum3(std::vector&lt;float&gt; &amp; v) { return std::accumulate(v.begin(), v.end(), 0.0f); }

标签: c++ arrays performance vector stdvector


【解决方案1】:

我认为开销来自于传递向量。

尝试传递引用:

float sum2(std::vector<float>& v)

【讨论】:

  • 哦,它在那里复制向量。太好了,就是这样,谢谢你,还有@generic_opto_guy 和@PaulMcKenzie!
  • 如果我按引用调用,剩下的性能差异是3.7%,所以这里的大部分问题确实是按值调用。不过,其他地方还是有一些性能损失?
  • @2xB sum2 调用 sum1。也许是呼叫的设置导致了问题。
  • 剩余的性能差异似乎纯粹是在统计上占主导地位。因此,通过引用调用确实是这里的一个重要区别。
  • @2xB 如果此答案解决了您的问题(您的第一条评论似乎就是这种情况),您可以考虑接受它:)
【解决方案2】:

您的函数sum2() 接受std::vector&lt;float&gt; 对象按值:

float sum2(std::vector<float> v) {
    return sum1(&v[0], v.size());
}

在这样的场景中:

std::vector<float> vec;
// ...
sum2(vec); // copies vec

参数对象v 导致从传递给sum2() 的参数vec 复制初始化。这可能是一项昂贵的操作,尤其是在向量很大的情况下。如果您的目标是降低与调用 sum2() 相关的开销,您可以选择:

  • 让sum2() 接受对std::vector&lt;float&gt; 的引用,即std::vector&lt;float&gt;&amp;:

    float sum2(std::vector<float>& v) {
       return sum1(&v[0], v.size());
    }
    

    在这种情况下,只是将向量的引用传递给函数,而不是整个向量,因此不会创建向量的副本。

  • 以某种方式调用sum2(),使其参数对象v从传递的参数中移动初始化(而不是复制初始化——你当前的状态做)如果您在调用sum2() 后不再需要vec 的内容:

    sum2(std::move(vec)); // move instead of copy
    

【讨论】:

  • sum2(std::move(vec)) 返回 0,所以使用它有问题。
  • @2xB 因为您在循环中调用sum2(std::move(vec))。第一次迭代后,vec 为空。
  • @2xB 您的循环仅用于基准测试目的。如果您有一个要在计算其元素之和后立即销毁的向量,则在将其作为参数传递给 sum2() 时,最好移动它而不是复制它。
  • 对于这种情况,我发现 move 是错误的。 sum 没有理由消耗输入。它甚至不修改输入。
  • @generic_opto_guy 我只是想公开这个概念并解释正在发生的事情。如果 OP 真的希望 sum2() 接受 std::vector&lt;float&gt; by value 并且不再需要参数,那么这种方法将有助于加快调用速度。当然,没有理由消耗输入,但如果你之后不需要向量,你就真的不在乎了。
【解决方案3】:

添加到使用引用以避免昂贵的向量副本的已建立答案:

当使用引用时,您可以使用 const 引用。

你需要改变

float sum1(float* v, int length)

到

float sum1(const float* v, int length)

和

float sum2(std::vector<float> v)

到

float sum2(const std::vector<float>& v)

使用引用意味着您不复制向量,但这也允许sum2 对向量进行更改。由于您使用引用的原因只是为了避免复制,我认为最好声明sum2 不会更改其界面中的向量。

const 的相同逻辑适用于sum1 并且变得无关紧要,因为 const 向量仅提供指向 const 的指针。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-02-13
    • 2023-03-27
    • 1970-01-01
    • 2017-03-15
    • 2013-05-14
    • 2023-02-04
    • 1970-01-01
    相关资源
    最近更新 更多