当时我正在阅读“现代C++编程手册,第二版”关于并发性的第8章,偶然发现了一些令我困惑的东西。
作者使用std::thread和std::async实现了不同版本的并行映射和精简函数。实现非常接近;例如,parallel_map函数的核心是
// 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是线程的更高层次的实现,它至少应该花费与线程相同的时间(如果不是的话) --显然我错了。这些发现是否与书中所建议的一致,其解释是什么?
发布于 2021-04-10 14:24:33
我原来的解释是不正确的。-- 请参阅以下@OznOg的答复。
修改答案:
我创建了一个简单的基准测试,它使用std::async和std::thread来执行一些小任务:
#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";
}我运行基准如下:
$ 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的基准测试,并累积了系统调用:
# 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的速度要快得多,但其他的一切似乎都没有那么重要。
我的结论是:
std::async不需要每个任务的新线程这一事实。std::async中执行某种std::thread不做的锁定。std::async与std::launch::deferred节省了安装和销毁成本,而且在这种情况下速度要快得多。我的机器配置如下:
$ 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节省了创建和销毁线程的开销。
(根据操作系统的不同,运行大量线程也可能导致性能下降。操作系统可能有一个调度策略,它试图保证每一个线程每隔一次执行一次,因此调度程序可以决定给单个线程更小的处理时间,从而为线程之间的切换带来更多的开销。)
发布于 2021-04-17 13:01:24
看上去有些事情不像预期的那样发生。我把整件事都写在了我的软呢帽上,第一个结果令人惊讶。我注释掉了所有的测试,只保留了两个比较线程和异步的测试。输出看起来像是确认了行为:
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系统调用的数量(创建的线程数):
64 clone with threads
92 clone with async因此,看起来创建线程所花费的时间上的解释是矛盾的,因为异步版本实际上创建的线程数量与基于线程的线程相同(区别在于异步代码中有两个版本的折叠)。
然后,我尝试交换执行的两个测试顺序(将异步放在线程之前),下面是结果:
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没有显示出任何差异:
92 clone
64 clone我没有太多的时间进行进一步的研究,但至少在linux上,我们可以考虑这两个版本之间没有什么区别(异步甚至可以被看作是效率较低的,因为它需要更多的线程)。
我们可以看到,它与异步/线程问题无关。
此外,如果我们看一下实际需要计算时间的值,时间的差异是很小的,而且不相关: 55752us对56785 is对于5000‘000,并且继续匹配更大的值。
这看起来像微工作台的常见问题,我们以某种方式测量系统的潜伏期,而不是计算时间本身。
注意:显示的数字没有优化(原始代码);添加-O3明显加快了计算速度,但结果表明:大值的计算时间没有真正的差异。
https://stackoverflow.com/questions/67034861
复制相似问题