首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么使用std::异步并发比使用std::线程更快?

为什么使用std::异步并发比使用std::线程更快?
EN

Stack Overflow用户
提问于 2021-04-10 13:26:03
回答 2查看 2.6K关注 0票数 27

当时我正在阅读“现代C++编程手册,第二版”关于并发性的第8章,偶然发现了一些令我困惑的东西。

作者使用std::threadstd::async实现了不同版本的并行映射和精简函数。实现非常接近;例如,parallel_map函数的核心是

代码语言:javascript
复制
// parallel_map using std::async
...
tasks.emplace_back(std::async(
  std::launch::async,
  [=, &f] {std::transform(begin, last, begin, std::forward<F>(f)); }));
...

// parallel_map using std::thread
...
threads.emplace_back([=, &f] {std::transform(begin, last, begin, std::forward<F>(f)); });
...

完整的代码可以找到这里std::thread那里std::async

令我困惑的是,书中报告的计算时间为std::async实现提供了显著和一致的优势。此外,提交人承认这一事实是显而易见的,但没有提供任何理由:

如果我们将这个结果与异步与使用线程的并行版本的结果进行比较,我们会发现这些结果的执行时间更快,而且速度非常快,特别是对于fold函数。

我在我的计算机上运行了上面的代码,尽管它们之间的区别并不像书中那样引人注目,但我发现std::async实现确实比std::thread实现更快。(作者随后还引入了这些算法的标准实现,它们甚至更快)。在我的计算机上,代码运行四个线程,这对应于我的CPU的物理核的数量。

也许我遗漏了一些东西,但是为什么在这个示例中std::async的运行速度应该比std::thread更快呢?我的直觉是,std::async是线程的更高层次的实现,它至少应该花费与线程相同的时间(如果不是的话) --显然我错了。这些发现是否与书中所建议的一致,其解释是什么?

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2021-04-10 14:24:33

我原来的解释是不正确的。-- 请参阅以下@OznOg的答复。

修改答案:

我创建了一个简单的基准测试,它使用std::asyncstd::thread来执行一些小任务:

代码语言:javascript
复制
#include <thread>
#include <chrono>
#include <vector>
#include <future>
#include <iostream>

__thread volatile int you_shall_not_optimize_this;

void work() {
    // This is the simplest way I can think of to prevent the compiler and
    // operating system from doing naughty things
    you_shall_not_optimize_this = 42;
}

[[gnu::noinline]]
std::chrono::nanoseconds benchmark_threads(size_t count) {
    std::vector<std::optional<std::thread>> threads;
    threads.resize(count);

    auto before = std::chrono::high_resolution_clock::now();

    for (size_t i = 0; i < count; ++i)
        threads[i] = std::thread { work };

    for (size_t i = 0; i < count; ++i)
        threads[i]->join();

    threads.clear();

    auto after = std::chrono::high_resolution_clock::now();

    return after - before;
}

[[gnu::noinline]]
std::chrono::nanoseconds benchmark_async(size_t count, std::launch policy) {
    std::vector<std::optional<std::future<void>>> results;
    results.resize(count);

    auto before = std::chrono::high_resolution_clock::now();

    for (size_t i = 0; i < count; ++i)
        results[i] = std::async(policy, work);

    for (size_t i = 0; i < count; ++i)
        results[i]->wait();

    results.clear();

    auto after = std::chrono::high_resolution_clock::now();

    return after - before;
}

std::ostream& operator<<(std::ostream& stream, std::launch value)
{
    if (value == std::launch::async)
        return stream << "std::launch::async";
    else if (value == std::launch::deferred)
        return stream << "std::launch::deferred";
    else
        return stream << "std::launch::unknown";
}

// #define CONFIG_THREADS true
// #define CONFIG_ITERATIONS 10000
// #define CONFIG_POLICY std::launch::async

int main() {
    std::cout << "Running benchmark:\n"
              << "  threads?     " << std::boolalpha << CONFIG_THREADS << '\n'
              << "  iterations   " << CONFIG_ITERATIONS << '\n'
              << "  async policy " << CONFIG_POLICY << std::endl;

    std::chrono::nanoseconds duration;
    if (CONFIG_THREADS) {
        duration = benchmark_threads(CONFIG_ITERATIONS);
    } else {
        duration = benchmark_async(CONFIG_ITERATIONS, CONFIG_POLICY);
    }

    std::cout << "Completed in " << duration.count() << "ns (" << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() << "ms)\n";
}

我运行基准如下:

代码语言:javascript
复制
$ g++ -Wall -Wextra -std=c++20 -pthread -O3 -DCONFIG_THREADS=false -DCONFIG_ITERATIONS=10000 -DCONFIG_POLICY=std::launch::deferred main.cpp -o main && ./main
Running benchmark:
  threads?     false
  iterations   10000
  async policy std::launch::deferred
Completed in 4783327ns (4ms)
$ g++ -Wall -Wextra -std=c++20 -pthread -O3 -DCONFIG_THREADS=false -DCONFIG_ITERATIONS=10000 -DCONFIG_POLICY=std::launch::async main.cpp -o main && ./main
Running benchmark:
  threads?     false
  iterations   10000
  async policy std::launch::async
Completed in 301756775ns (301ms)
$ g++ -Wall -Wextra -std=c++20 -pthread -O3 -DCONFIG_THREADS=true -DCONFIG_ITERATIONS=10000 -DCONFIG_POLICY=std::launch::deferred main.cpp -o main && ./main
Running benchmark:
  threads?     true
  iterations   10000
  async policy std::launch::deferred
Completed in 291284997ns (291ms)
$ g++ -Wall -Wextra -std=c++20 -pthread -O3 -DCONFIG_THREADS=true -DCONFIG_ITERATIONS=10000 -DCONFIG_POLICY=std::launch::async main.cpp -o main && ./main
Running benchmark:
  threads?     true
  iterations   10000
  async policy std::launch::async
Completed in 293539858ns (293ms)

我重新运行了所有带有strace的基准测试,并累积了系统调用:

代码语言:javascript
复制
# std::async with std::launch::async
      1 access
      2 arch_prctl
     36 brk
  10000 clone
      6 close
      1 execve
      1 exit_group
  10002 futex
  10028 mmap
  10009 mprotect
   9998 munmap
      7 newfstatat
      6 openat
      7 pread64
      1 prlimit64
      5 read
      2 rt_sigaction
  20001 rt_sigprocmask
      1 set_robust_list
      1 set_tid_address
      5 write

# std::async with std::launch::deferred
      1 access
      2 arch_prctl
     11 brk
      6 close
      1 execve
      1 exit_group
  10002 futex
     28 mmap
      9 mprotect
      2 munmap
      7 newfstatat
      6 openat
      7 pread64
      1 prlimit64
      5 read
      2 rt_sigaction
      1 rt_sigprocmask
      1 set_robust_list
      1 set_tid_address
      5 write

# std::thread with std::launch::async
      1 access
      2 arch_prctl
     27 brk
  10000 clone
      6 close
      1 execve
      1 exit_group
      2 futex
  10028 mmap
  10009 mprotect
   9998 munmap
      7 newfstatat
      6 openat
      7 pread64
      1 prlimit64
      5 read
      2 rt_sigaction
  20001 rt_sigprocmask
      1 set_robust_list
      1 set_tid_address
      5 write

# std::thread with std::launch::deferred
      1 access
      2 arch_prctl
     27 brk
  10000 clone
      6 close
      1 execve
      1 exit_group
      2 futex
  10028 mmap
  10009 mprotect
   9998 munmap
      7 newfstatat
      6 openat
      7 pread64
      1 prlimit64
      5 read
      2 rt_sigaction
  20001 rt_sigprocmask
      1 set_robust_list
      1 set_tid_address
      5 write

我们观察到,std::async使用std::launch::deferred的速度要快得多,但其他的一切似乎都没有那么重要。

我的结论是:

  • 当前的libstdc++实现没有利用std::async不需要每个任务的新线程这一事实。
  • 当前的libstdc++实现在std::async中执行某种std::thread不做的锁定。
  • std::asyncstd::launch::deferred节省了安装和销毁成本,而且在这种情况下速度要快得多。

我的机器配置如下:

代码语言:javascript
复制
$ uname -a
Linux linux-2 5.12.1-arch1-1 #1 SMP PREEMPT Sun, 02 May 2021 12:43:58 +0000 x86_64 GNU/Linux
$ g++ --version
g++ (GCC) 10.2.0
Copyright (C) 2020 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.
$ lscpu # truncated
Architecture:                    x86_64
Byte Order:                      Little Endian
CPU(s):                          8
Model name:                      Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz

原始答案:

std::thread是由操作系统提供的线程对象的包装器,创建和销毁它们非常昂贵。

std::async类似,但在任务和操作系统线程之间没有1到1的映射。这可以通过线程池来实现,其中线程被重用用于多个任务。

因此,如果您有许多小任务,std::async会更好,如果您有几个长时间运行的任务,std::thread会更好。

另外,如果您确实需要并行进行一些事情,那么std::async可能不太适合。(std::thread也不能做出这样的保证,但这是你能得到的最接近的保证。)

也许为了澄清一下,在您的例子中,std::async节省了创建和销毁线程的开销。

(根据操作系统的不同,运行大量线程也可能导致性能下降。操作系统可能有一个调度策略,它试图保证每一个线程每隔一次执行一次,因此调度程序可以决定给单个线程更小的处理时间,从而为线程之间的切换带来更多的开销。)

票数 24
EN

Stack Overflow用户

发布于 2021-04-17 13:01:24

看上去有些事情不像预期的那样发生。我把整件事都写在了我的软呢帽上,第一个结果令人惊讶。我注释掉了所有的测试,只保留了两个比较线程和异步的测试。输出看起来像是确认了行为:

代码语言:javascript
复制
 Thead version result

    size   s map   p map  s fold  p fold
   10000     642     628     751     770
  100000    6533    3444    7985    3338
  500000   14885    5760   13854    6304
 1000000   23428   11398   27795   12129
 2000000   47136   22468   55518   24154
 5000000  118690   55752  139489   60142
10000000  236496  112467  277413  121002
25000000  589277  276750  694742  297832
500000001.17839e+06  5553181.39065e+06  594102

Async version:
    size   s map  p1 map  p2 map  s fold p1 fold p2 fold
   10000     248     232     231     273     282     273
  100000    2323    1562    2827    2757    1536    1766
  500000   12312    5615   12044   14014    6272    7431
 1000000   23585   11701   24060   27851   12376   14109
 2000000   47147   22796   48035   55433   25565   30095
 5000000  118465   59980  119698  140775   62960   68382
10000000  241727  110883  239554  277958  121205  136041

看起来异步的速度实际上是线程的2倍(对于小值)。然后,我使用strace来计算已完成的clone系统调用的数量(创建的线程数):

代码语言:javascript
复制
 64 clone with threads
 92 clone with async

因此,看起来创建线程所花费的时间上的解释是矛盾的,因为异步版本实际上创建的线程数量与基于线程的线程相同(区别在于异步代码中有两个版本的折叠)。

然后,我尝试交换执行的两个测试顺序(将异步放在线程之前),下面是结果:

代码语言:javascript
复制
    size   s map  p1 map  p2 map  s fold p1 fold p2 fold
   10000     653     694     624     718     748     718
  100000    6731    3931    2978    8533    3116    1724
  500000   12406    5839   14589   13895    8427    7072
 1000000   23813   11578   24099   27853   13091   14108
 2000000   47357   22402   48197   55469   24572   33543
 5000000  117923   55869  120303  139061   61801   68281
10000000  234861  111055  239124  277153  121270  136953
    size   s map   p map  s fold  p fold
   10000     232     232     273     328
  100000    6424    3271    8297    4487
  500000   21329    5547   13913    6263
 1000000   23654   11419   27827   12083
 2000000   47230   22763   55653   24135
 5000000  117448   56785  139286   61679
10000000  235394  111021  278177  119805
25000000  589329  279637  696392  301485
500000001.1824e+06  5564431.38722e+06  606279

因此,对于小值,“线程”版本比异步快2倍。

查看克隆调用,di没有显示出任何差异:

代码语言:javascript
复制
 92 clone
 64 clone

我没有太多的时间进行进一步的研究,但至少在linux上,我们可以考虑这两个版本之间没有什么区别(异步甚至可以被看作是效率较低的,因为它需要更多的线程)。

我们可以看到,它与异步/线程问题无关。

此外,如果我们看一下实际需要计算时间的值,时间的差异是很小的,而且不相关: 55752us对56785 is对于5000‘000,并且继续匹配更大的值。

这看起来像微工作台的常见问题,我们以某种方式测量系统的潜伏期,而不是计算时间本身。

注意:显示的数字没有优化(原始代码);添加-O3明显加快了计算速度,但结果表明:大值的计算时间没有真正的差异。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/67034861

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档