0

我正在尝试使用 std::async 来填充向量。它背后的想法是使用多线程来节省时间。然而,运行一些基准测试我发现我的非异步方法更快!

#include <algorithm>
#include <vector>
#include <future>

std::vector<int> Generate(int i)
{
    std::vector<int> v;
    for (int j = i; j < i + 10; ++j)
    {
        v.push_back(j);
    }
    return v;
}

异步:

std::vector<std::future<std::vector<int>>> futures;
for (int i = 0; i < 200; i+=10)
{
  futures.push_back(std::async(
    [](int i) { return Generate(i); }, i));
}

std::vector<int> res;
for (auto &&f : futures)
{
  auto vec = f.get();
  res.insert(std::end(res), std::begin(vec), std::end(vec));
}

非异步:

std::vector<int> res;
for (int i = 0; i < 200; i+=10)
{
   auto vec = Generate(i);
   res.insert(std::end(res), std::begin(vec), std::end(vec));
}

我的基准测试表明异步方法比非异步方法慢 71 倍。我究竟做错了什么?

4

3 回答 3

4

std::async有两种操作模式:

  1. std::launch::async
  2. std::launch::deferred

在这种情况下,您调用std::async时没有指定任何一个,这意味着它可以选择任何一个。std::launch::deferred基本上意味着在调用线程上做工作。所以std::async返回 afuture和 with std::launch::deferred,您请求的操作将不会执行,直到您调用.getfuture。在某些情况下它可能会很方便,但这可能不是您想要的。

即使您指定std::launch::async,您也需要意识到这会启动一个新的执行线程来执行您请求的操作。然后它必须创建一个future, 并使用某种从线程到未来的信号来让您知道您请求的计算何时完成。

所有这些都会增加相当多的开销——从微秒到毫秒左右,取决于操作系统、CPU 等。

因此,要使异步执行有意义,您以异步方式执行的“工作”通常至少需要花费数十毫秒(数百毫秒可能是更合理的较低阈值)。我不会太纠结于确切的截止日期,但这需要一些时间。

因此,只有当数组比您在此处处理的要大得多时,异步填充数组才有意义。

对于填充内存,您很快就会遇到另一个问题:大多数 CPU 都比主内存快得多,如果您所做的只是写入内存,那么单个线程很有可能已经使内存路径饱和,因此即使充其量异步完成这项工作也只会获得一点好处,并且仍然很容易导致速度变慢。

异步操作的理想情况是一个线程受大量内存限制,而另一个线程(例如)读取少量数据,并对少量数据进行大量计算。在这种情况下,计算线程将主要对其缓存中的数据进行操作,因此它不会妨碍内存线程执行其操作。

于 2019-08-27T21:55:59.250 回答
3

有多种因素导致多线程代码的执行速度比单线程代码慢得多。

您的数组大小太小

多线程通常对特别小的数据集几乎没有影响。在您的代码的两个版本中,您都生成了 2000 个整数,并且每个逻辑线程(因为std::async通常根据线程池实现,可能与软件线程不同)仅生成 10 个整数。以每 10 个整数方式假脱机线程的成本抵消了并行生成这些整数的好处。

如果每个线程负责,例如,每个线程负责 10,000 个整数,您可能会看到性能提升,但您可能会遇到不同的问题:

您的所有代码都受到固有串行过程的瓶颈

两个版本的代码都将生成的整数复制到主向量中。如果生成这些整数的行为本身是一个耗时的过程,那将是一回事,但在您的情况下,这可能只是生成每个整数的一小部分快速组装的问题。

因此,将每个整数复制到最终向量中的行为本质上可能并不比生成每个整数更快,这意味着正在完成的“工作”中相当大的一部分是完全串行的,这违背了多线程代码的整个目的。

修复代码

编译器非常擅长他们的工作,因此在尝试修改您的代码时,我只能勉强获得比串行代码更快的多线程代码。多次执行有不同的结果,所以我的总体评估是这种代码不适合多线程。

但这是我想出的:

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

//#1: Constants
constexpr int BLOCK_SIZE = 500000;
constexpr int NUM_OF_BLOCKS = 20;

std::vector<int> Generate(int i) {
    std::vector<int> v;
    for (int j = i; j < i + BLOCK_SIZE; ++j) {
        v.push_back(j);
    }
    return v;
}

void asynchronous_attempt() {
    std::vector<std::future<void>> futures;
    //#2: Preallocated Vector
    std::vector<int> res(NUM_OF_BLOCKS * BLOCK_SIZE);
    auto it = res.begin();
    for (int i = 0; i < NUM_OF_BLOCKS * BLOCK_SIZE; i+=BLOCK_SIZE)
    {
      futures.push_back(std::async(
        [it](int i) { 
            auto vec = Generate(i); 
            //#3 Copying done multithreaded
            std::copy(vec.begin(), vec.end(), it + i);
        }, i));
    }
    
    for (auto &&f : futures) {
        f.get();
    }
}

void serial_attempt() {
    //#4 Changes here to show fair comparison
    std::vector<int> res(NUM_OF_BLOCKS * BLOCK_SIZE);
    auto it = res.begin();
    for (int i = 0; i < NUM_OF_BLOCKS * BLOCK_SIZE; i+=BLOCK_SIZE) {
        auto vec = Generate(i);
        it = std::copy(vec.begin(), vec.end(), it);
    }
}

int main() {
    using clock = std::chrono::steady_clock;

    std::cout << "Theoretical # of Threads: " << std::thread::hardware_concurrency() << std::endl;
    auto begin = clock::now();
    asynchronous_attempt();
    auto end = clock::now();
    std::cout << "Duration of Multithreaded Attempt: " << std::setw(10) << (end - begin).count() << "ns" << std::endl;
    begin = clock::now();
    serial_attempt();
    end = clock::now();
    std::cout << "Duration of Serial Attempt:        " << std::setw(10) << (end - begin).count() << "ns" << std::endl;
}

这导致以下输出:

Theoretical # of Threads: 2
Duration of Multithreaded Attempt:  361149213ns
Duration of Serial Attempt:         364785676ns

鉴于这是在在线编译器上(此处),我敢打赌,多线程代码可能会在专用机器上胜出,但我认为这至少证明了性能的提高,我们至少在两者之间相当方法。

以下是我所做的更改,这些更改已在代码中标识:

  1. 我们显着增加了正在生成的整数的数量,以强制线程执行实际有意义的工作,而不是陷入操作系统级别的内务管理中
  2. 该向量具有预先分配的大小。不再频繁调整大小。
  3. 现在空间已经被预先分配,我们可以多线程复制而不是稍后以串行方式进行。
  4. 我们必须更改序列号,以便它也预先分配 + 副本,以便进行公平比较。

现在,我们已经确保所有代码确实是并行运行的,虽然它并没有对串行代码进行实质性改进,但至少不再表现出我们之前看到的退化性能损失。

于 2019-08-27T22:07:07.927 回答
2

First of all, you are not forcing the std::async to work asynchronously (you would need to specify std::launch::async policy to do so). Second of all, it'd be kind of an overkill to asynchronously create an std::vector of 10 ints. It's just not worth it. Remember - using more threads does not mean that you will see performance benefit! Creating a thread (or even using a threadpool) introduces some overhead, which, in this case, seems to dwarf the benefits of running tasks asynchronously.

Thanks @NathanOliver ;>

于 2019-08-27T21:30:43.953 回答