C++17在标准库中增加了对并行算法的支持,以帮助程序利用并行执行的优势来提高性能。MSVC在15.5中首次为一些算法增加了实验性支持,在15.7中删除了实验性标签。
标准中描述的并行算法的接口并没有确切地说明一个给定的工作负载是如何被并行化的。特别是,该接口旨在以一种适用于异构机器的通用形式来表达并行,允许像SSE、AVX或NEON那样的SIMD并行,像GPU编程模型中的矢量"通道"那样的并行,以及传统的线程并行。
我们的并行算法实现目前完全依赖于库的支持,而不是编译器的特殊支持。这意味着我们的实现将与目前消费我们标准库的任何工具一起工作,而不仅仅是MSVC的编译器。特别是,我们测试了它与Clang/LLVM和支持Intellisense的EDG版本的工作关系。
要使用并行算法库,你可以按照以下步骤进行。
.\debug.exeTestingwith1000000doubles…Serial:Lowest:1349Highest:4.29497e+09Time:310.176500msSerial:Lowest:1349Highest:4.29497e+09Time:304.714800msSerial:Lowest:1349Highest:4.29497e+09Time:310.345800msSerial:Lowest:1349Highest:4.29497e+09Time:303.302200msSerial:Lowest:1349Highest:4.29497e+09Time:290.694300msC:\Users\bion\Desktop>.\release.exeTestingwith1000000doubles…Serial:Lowest:2173Highest:4.29497e+09Time:74.590400msSerial:Lowest:2173Highest:4.29497e+09Time:75.703500msSerial:Lowest:2173Highest:4.29497e+09Time:87.839700msSerial:Lowest:2173Highest:4.29497e+09Time:73.822300msSerial:Lowest:2173Highest:4.29497e+09Time:73.757400ms接下来,我们需要确保我们的排序调用是安全的,可以并行化。如果"元素访问函数"--也就是迭代器操作、谓词以及你要求算法代表你做的其他任何事情都遵循正常的"任意数量的读者或最多一个写者"的数据竞赛规则,那么算法的并行化是安全的。此外,它们必须不抛出异常(或者抛出的异常很少,以至于在它们抛出时终止程序是可以的)。
接下来,选择一个执行策略。目前,该标准包括并行策略,由std::execution::par表示,以及并行无序列策略,由std::execution::par_unseq表示。除了并行策略所暴露的要求外,并行无序策略还要求你的元素访问函数能够容忍比并发的前向进度保证更弱的内容。这意味着它们不加锁或以其他方式执行需要线程并发执行以取得进展的操作。例如,如果一个并行算法在GPU上运行并试图取得一个自旋锁,在自旋锁上旋转的线程可能会阻止GPU上的其他线程执行,这意味着持有自旋锁的线程可能永远不会解锁,从而使程序陷入死锁。你可以在C++标准的[algorithms.parallel.defns]和[algorithms.parallel.exec]部分阅读更多关于具体的要求。如果有疑问,请使用并行策略。在这个例子中,我们使用的是不需要任何锁的内置双倍小于运算符,以及标准库提供的迭代器类型,所以我们可以使用并行的无序策略。
请注意,VisualC++的实现以同样的方式实现了并行和并行不排序策略,所以你不应该期望在我们的实现上使用par_unseq来获得更好的性能,但是有一天可能存在可以使用这种额外自由的实现。
在上面的双打排序例子中,我们现在可以添加
我们建立了并行反向,在我们的测试硬件上它比串行版本慢了1.6倍,即使是大的N值。这并不意味着标准委员会在STL中加入这些算法是错误的;这只是意味着我们的实现所针对的硬件没有看到改进。因此,我们提供了签名,但实际上并没有对那些只是按顺序排列的元素进行交换、复制或移动的算法进行并行化。如果我们得到反馈,有一个并行化会更快的例子,我们将研究这些算法的并行化。受影响的算法是。
copycopy_nfillfill_nmovereversereverse_copyrotaterotate_copyswap_ranges有些算法目前还没有实现,将在未来的版本中完成。我们在VisualStudio201715.8中并行化的算法是。
adjacent_differenceadjacent_findall_ofany_ofcountcount_ifequalexclusive_scanfindfind_endfind_first_offind_iffor_eachfor_each_ninclusive_scanmismatchnone_ofpartitionreduceremoveremove_ifsearchsearch_nsortstable_sorttransformtransform_exclusive_scantransform_inclusive_scantransform_reduceMSVC的并行算法实现的设计目标虽然标准规定了并行算法库的接口,但它根本没有说算法应该如何并行化,甚至没有说算法应该在什么硬件上并行化。一些C++的实现可以通过使用GPU或其他异构计算硬件来实现并行化,如果目标上有的话。对于我们的实现来说,复制并行化是没有意义的,但对于以GPU或类似加速器为目标的实现来说,这确实是有意义的。在我们的实现中,我们重视以下几个方面。
微软之前推出了一个并行化框架ConcRT,它为标准库的部分内容提供了支持。ConcRT允许不同的工作负载透明地使用可用的硬件,并让线程完成彼此的工作,这可以提高整体吞吐量。基本上,每当一个线程在运行ConcRT工作负载时通常会进入睡眠状态,它就会暂停当前正在执行的工作,而运行其他准备运行的工作。这种非阻塞行为减少了上下文切换,可以产生比我们的并行算法实现所使用的Windows线程池更高的整体吞吐量。然而,这也意味着ConcRT工作负载不能与操作系统同步原语,如SRWLOCK、NT事件、semaphores、COM单线程公寓、窗口程序等组成。我们认为,对于标准库中的"默认"实现来说,这是一个不可接受的折衷。
我们关心的是调试性能。那些需要打开优化器才能实用的解决方案并不适合在标准库中使用。如果我在前面的例子程序中添加一个Concurrency::parallel_sort的调用,ConcRT的并行排序在发布时要快一点,但在调试时几乎慢了100倍。
关于线程池代表你(和我们)所做的各种优化的更多信息,请查看PedroTeixeira关于线程池的演讲,以及CreateThreadpoolWork、SubmitThreadpoolWork、WaitForThreadpoolWorkCallbacks和CloseThreadpoolWork函数的官方文档。
如果我们不能想出一个实用的基准,让并行算法在合理的N值下获胜,那么它将不会被并行化。我们认为在N=1'000'000'000时速度是两倍,而在N=100时速度慢3个数量级是不可接受的折衷。如果你想要"不计成本的并行化",有很多其他的实现可以和MSVC一起使用,包括HPX和线程构件。
同样地,C++标准允许并行算法分配内存,并在无法获得内存时抛出std::bad_alloc。在我们的实现中,如果不能获得额外的资源,我们会退回到算法的串行版本。