为什么在使用 std::async 时通过 const ref 的速度变慢
Why is passing by const ref slower when using std::async
作为学习 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::async
函数调用中使用 std::ref(v)
,我的函数会更快更高效。这对我来说仍然有意义。
但是,如果我仍然用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
进行编译(在仅通过 v
时添加 -O3
演示了相同的效果)
g++ --version
g++ (Rev1, Built by MSYS2 project) 6.3.0
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
正如人们所说,没有 std::ref 正在复制对象。
现在我认为按值传递实际上更快的原因可能与以下问题有关:Is it better in C++ to pass by value or pass by constant reference?
可能会发生什么,在异步的内部实现中,向量被复制一次到新线程。然后在内部通过引用传递给一个拥有向量所有权的函数,这意味着它将再次被复制。另一方面,如果你按值传递它,它会将它复制一次到新线程,但会在新线程内移动它两次。如果对象通过引用传递,则产生 2 个副本,如果对象通过值传递,则在第二种情况下产生 1 个副本和 2 个移动。
短篇小说
给定一个可复制和移动构造类型T
V f(T);
V g(T const&);
T t;
auto v = std::async(f,t).get();
auto v = std::async(g,t).get();
这两个异步调用的唯一相关区别是,在第一个异步调用中,t 的副本在 f returns 后立即销毁;第二,根据 get() 调用的效果,t 的副本 可能 被销毁。
如果异步调用发生在一个循环中,未来是 get() 之后,第一个将具有平均不变的内存(假设每线程工作负载不变),第二个线性增长内存在最坏的情况下,导致更多的缓存命中和更差的分配性能。
说来话长
首先,我可以在 gcc 和 clang 上重现观察到的减速(在我的系统中一直是 ~2x);此外,具有等效 std::thread
调用的相同代码确实 not 表现出相同的行为,const& 版本如预期的那样稍微快一些。让我们看看为什么。
首先,async 的规范如下:
[futures.async] If launch::async is set in policy, calls INVOKE(DECAY_COPY(std::forward(f)), DECAY_COPY(std::forward(args))...) (23.14.3, 33.3.2.2) as if in a new thread of execution represented by a thread object with the calls to DECAY_COPY() being evaluated in the thread that called async[...]The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.
因此,async 将复制参数并将这些副本转发给可调用对象,从而保留右值性;在这方面,它和std::thread
构造函数一样,两个OP版本没有区别,都是复制vector。
不同之处在于粗体部分:线程对象是共享状态的一部分,在后者被释放之前不会被释放(例如通过future::get() 调用).
为什么这很重要?因为标准没有指定衰减的副本绑定到谁,我们只知道它们必须比可调用的调用长寿,但我们不知道知道它们是否会在调用后或在线程退出时或线程对象被销毁时立即销毁(连同共享状态)。
事实上,事实证明 gcc 和 clang 实现将衰减的副本存储在结果未来的共享状态中。
因此,在 const& 版本中,矢量副本存储在共享状态并在 future::get
销毁:这导致“开始threads”循环在每一步分配一个新向量,内存线性增长。
相反,在 by-value 版本中,向量副本在可调用参数中移动,并在可调用 returns 时立即销毁;在 future::get
,一个移动的空向量将被销毁。因此,如果可调用对象的速度足够快,可以在创建新向量之前销毁该向量,将一遍又一遍地分配相同的向量,内存将保持几乎不变。这将导致更少的缓存命中和更快的分配,解释改进的时间。
作为学习 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::async
函数调用中使用 std::ref(v)
,我的函数会更快更高效。这对我来说仍然有意义。
但是,如果我仍然用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
进行编译(在仅通过 v
时添加 -O3
演示了相同的效果)
g++ --version
g++ (Rev1, Built by MSYS2 project) 6.3.0 Copyright (C) 2016 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
正如人们所说,没有 std::ref 正在复制对象。
现在我认为按值传递实际上更快的原因可能与以下问题有关:Is it better in C++ to pass by value or pass by constant reference?
可能会发生什么,在异步的内部实现中,向量被复制一次到新线程。然后在内部通过引用传递给一个拥有向量所有权的函数,这意味着它将再次被复制。另一方面,如果你按值传递它,它会将它复制一次到新线程,但会在新线程内移动它两次。如果对象通过引用传递,则产生 2 个副本,如果对象通过值传递,则在第二种情况下产生 1 个副本和 2 个移动。
短篇小说
给定一个可复制和移动构造类型T
V f(T);
V g(T const&);
T t;
auto v = std::async(f,t).get();
auto v = std::async(g,t).get();
这两个异步调用的唯一相关区别是,在第一个异步调用中,t 的副本在 f returns 后立即销毁;第二,根据 get() 调用的效果,t 的副本 可能 被销毁。 如果异步调用发生在一个循环中,未来是 get() 之后,第一个将具有平均不变的内存(假设每线程工作负载不变),第二个线性增长内存在最坏的情况下,导致更多的缓存命中和更差的分配性能。
说来话长
首先,我可以在 gcc 和 clang 上重现观察到的减速(在我的系统中一直是 ~2x);此外,具有等效 std::thread
调用的相同代码确实 not 表现出相同的行为,const& 版本如预期的那样稍微快一些。让我们看看为什么。
首先,async 的规范如下:
[futures.async] If launch::async is set in policy, calls INVOKE(DECAY_COPY(std::forward(f)), DECAY_COPY(std::forward(args))...) (23.14.3, 33.3.2.2) as if in a new thread of execution represented by a thread object with the calls to DECAY_COPY() being evaluated in the thread that called async[...]The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.
因此,async 将复制参数并将这些副本转发给可调用对象,从而保留右值性;在这方面,它和std::thread
构造函数一样,两个OP版本没有区别,都是复制vector。
不同之处在于粗体部分:线程对象是共享状态的一部分,在后者被释放之前不会被释放(例如通过future::get() 调用).
为什么这很重要?因为标准没有指定衰减的副本绑定到谁,我们只知道它们必须比可调用的调用长寿,但我们不知道知道它们是否会在调用后或在线程退出时或线程对象被销毁时立即销毁(连同共享状态)。
事实上,事实证明 gcc 和 clang 实现将衰减的副本存储在结果未来的共享状态中。
因此,在 const& 版本中,矢量副本存储在共享状态并在 future::get
销毁:这导致“开始threads”循环在每一步分配一个新向量,内存线性增长。
相反,在 by-value 版本中,向量副本在可调用参数中移动,并在可调用 returns 时立即销毁;在 future::get
,一个移动的空向量将被销毁。因此,如果可调用对象的速度足够快,可以在创建新向量之前销毁该向量,将一遍又一遍地分配相同的向量,内存将保持几乎不变。这将导致更少的缓存命中和更快的分配,解释改进的时间。