使用std :: async时,为什么传递const ref会变慢

时间:2017-11-22 14:16:09

标签: c++ stl pass-by-reference

作为一个了解std::async的练习,我写了一个小程序来计算大vector<int>的总和,分布在很多线程上。

我的代码如下

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

typedef unsigned long long int myint;

// Calculate sum of part of the elements in a vector
myint partialSum(const std::vector<myint>& v, int start, int end)
{
    myint sum(0);
    for(int i=start; i<=end; ++i)
    {
        sum += v[i];
    }
    return sum;
}

int main()
{
    const int nThreads = 100;
    const int sizePerThread = 100000;
    const int vectorSize = nThreads * sizePerThread;

    std::vector<myint> v(vectorSize);   
    std::vector<std::future<myint>> partial(nThreads);
    myint tot = 0;

    // Fill vector  
    for(int i=0; i<vectorSize; ++i)
    {
        v[i] = i+1;
    }
    std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();

    // Start threads
    for( int t=0; t < nThreads; ++t)
    {
        partial[t] = std::async( std::launch::async, partialSum, v, t*sizePerThread, (t+1)*sizePerThread -1);               
    }

    // Sum total
    for( int t=0; t < nThreads; ++t)
    {
        myint ps = partial[t].get();
        std::cout << t << ":\t" << ps << std::endl;
        tot += ps;
    }
    std::cout << "Sum:\t" << tot << std::endl;

    std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
    std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count() <<std::endl;
}

我的问题是关注函数partialSum的调用,特别是大向量的传递方式。该函数调用如下:

        partial[t] = std::async( std::launch::async, partialSum, v, t*sizePerThread, (t+1)*sizePerThread -1);       

定义如下

myint partialSum(const std::vector<myint>& v, int start, int end)

使用这种方法,计算相对较慢。如果我在std::ref(v)函数调用中使用std::async,我的函数会更快更有效。这对我来说仍然有意义。

但是,如果我仍然通过v拨打,而不是std::ref(v),而是用

替换该功能
myint partialSum(std::vector<myint> v, int start, int end)

程序也运行得更快(并且使用更少的内存)。我不明白为什么const ref实现比较慢。如果没有任何引用,编译器如何解决这个问题呢?

使用const ref实现,该程序通常需要6.2秒才能运行,而不需要3.0。 (注意,使用const ref和std::ref,它在0.2秒内运行)

我正在使用g++ -Wall -pedantic进行编译(在传递-O3时添加v表示同样的效果)

  

g ++ --version

     

g ++(Rev1,由MSYS2项目构建)6.3.0   版权所有(C)2016 Free Software Foundation,Inc。   这是免费软件;查看复制条件的来源。没有   保证;甚至不适用于适销性或特定用途的适用性。

2 个答案:

答案 0 :(得分:6)

短篇小说

给出副本和move-constructible 类型T

V f(T);
V g(T const&);

T t;
auto v = std::async(f,t).get();
auto v = std::async(g,t).get();

关于两个异步调用的唯一相关区别在于,在第一个中,只要f返回就会销毁t的副本;在第二个中,根据get()调用的效果,可以销毁副本 。 如果异步调用发生在循环中,未来将是 get(),则第一个将在avarage上具有常量内存(假设每个线程的工作负载不变),第二个是最差的线性增长内存,导致更多的缓存命中和更差的分配性能。

长篇故事

首先,我可以在gcc和clang上重现观察到的减速(在我的系统中始终为~2x);此外,具有等效std::thread调用的相同代码表现出相同的行为,其中const&amp;版本的结果比预期的要快一些。让我们看看为什么。

首先,异步读取的规范如下:

  

[futures.async] 如果在策略中设置了launch :: async,则调用INVOKE(DECAY_COPY(std :: forward(f)),DECAY_COPY(std :: forward(args))。 ..)(23.14.3,33.3.2.2)好像在一个新的执行线程中,由一个线程对象表示调用DECAY_COPY()在调用async的线程中被评估[...] 该线程object存储在共享状态中,并影响引用该状态的任何异步返回对象的行为。

所以,async会将转发这些副本的参数复制到callable,保留rvalueness;在这方面,它与std::thread构造函数相同,并且两个OP版本没有区别,都复制了向量。

区别在于粗体部分:线程对象是共享状态的一部分,并且在后者被释放之前不会被释放(例如,通过future :: get()调用)

为什么这很重要?因为标准没有指定腐朽副本的绑定对象,我们只知道他们必须比可调用的调用活动更长,但我们不知道他们是否将在调用之后或线程退出时或线程对象被销毁时(以及共享状态)立即销毁。

事实上,事实证明,gcc和clang实现将衰减的副本存储在结果未来的共享状态中。

因此,在 const&amp; 版本中,矢量副本存储在共享状态并在future::get处销毁:这会导致&#34;启动线程&#34 ;循环在每一步分配一个新的向量,内存线性增长

相反,在 by-value 版本中,矢量副本在callable参数中移动,并在callable返回后立即销毁;在future::get处,移动的空矢量将被销毁。因此,如果可调用对象足够快以在创建新向量之前销毁向量,则将反复分配相同的向量,并且内存将保持几乎恒定。这将导致更少的缓存命中和更快的分配,解释了改进的时间。

答案 1 :(得分:0)

正如人们所说,没有std :: ref正在复制对象。

现在我相信传递值实际上更快的原因可能与以下问题有关:Is it better in C++ to pass by value or pass by constant reference?

可能会发生什么,在异步的内部实现中,向量被复制一次到新线程。然后在内部通过引用传递给一个取得向量所有权的函数,这意味着它将被再次复制。另一方面,如果你按值传递它,它会将它复制一次到新线程,但是会在新线程内移动两次。如果通过引用传递对象,则产生2个副本;如果通过值传递对象,则产生1个副本和2个移动;