我正在研究F#中的多核并行性。我必须承认,不可变性确实有助于编写正确的并行实现。但是,当核心数量增长时,很难实现良好的加速和良好的可扩展性。例如,我使用快速排序算法的经验是,许多尝试以纯函数方式实现并行快速排序并使用List
或Array
作为表示失败。对这些实现进行分析表明,与顺序版本相比,缓存未命中数量显着增加。但是,如果使用阵列内部的突变实现并行快速排序,则可以获得良好的加速。因此,我认为突变可能是优化多核并行性的一种很好的实践。
我认为cache locality是功能语言中多核并行的一大障碍。函数式编程涉及创建许多短期对象;破坏这些对象可能会破坏CPU缓存的一致性。我已经看到很多建议如何改进命令式语言中的缓存局部性,例如here和here。但是我不清楚如何在函数式编程中完成它们,特别是对于常常出现的递归数据结构,例如树等等。
是否有任何技术可以改进不纯函数语言(特别是F#)中的缓存局部性?任何建议或代码示例都非常受欢迎。
答案 0 :(得分:24)
据我所知,缓存局部性(多线程或其他)的关键是
为此目的;
在实践中,这意味着你可能最终使用的数据结构在理论上并不是计算机科学的完美例子 - 但没关系,计算机在理论上也不是计算机科学的完美例子。
关于这个问题的一篇好的学术论文是Cache-Efficient String Sorting Using Copying
答案 1 :(得分:3)
在F#中允许函数内的可变性是一种祝福,但它只应在优化代码时使用。纯粹的功能风格通常会产生更直观的实现,因此是首选。
以下是快速搜索返回的内容:Parallel Quicksort in Haskell。让我们继续讨论关于性能的性能。选择一个处理器,然后使用特定的算法进行调整。
为了回答您的问题而没有具体细节,我会说Clojure实施STM的方法可能是关于如何在多核处理器上解耦执行路径和改进缓存的一般案例局部性。但它只有在读数超过写入次数时才有效。
答案 2 :(得分:3)
我不是并行专家,但无论如何,这是我的建议。
答案 3 :(得分:3)
一种很好的方法是将工作分成更小的部分,并迭代每个核心上的每个部分。
我开始的一个选择是在并行之前在单个核心上查找缓存局部性改进,它应该只是为每个核心再次细分工作。例如,如果您使用大型矩阵进行矩阵计算,则可以将计算分成更小的部分。
这是一个很好的例子:Cache Locality For Performance
Tomas Petricek的书中有一些很棒的部分实际功能编程,查看第14章编写并行功能程序,您可能会发现特别感兴趣的二叉树的并行处理。
答案 4 :(得分:2)
编写可扩展的应用缓存位置对于您的应用程序速度至关重要。 Scott Meyers谈话很好地解释了这些原则。由于您在内存中创建新对象,迫使CPU再次从新对象重新加载数据,因此不变性在缓存局部性方面不能很好地发挥作用。 正如在现代CPU中所说的那样,L1缓存只有32 KB大小,所有内核之间的代码和数据共享。如果你使用多线程,你应该尝试尽可能少地使用内存(再见不可变性)来保持最快的缓存。 L2缓存大约是4-8 MB,与您尝试排序的数据相比,它要大得多,但仍然很小。
如果您设法编写一个消耗尽可能少的内存(数据缓存位置)的应用程序,您可以获得20或更多的加速。但是如果你为1核处理这个问题,那么扩展到更多内核可能会损害性能,因为所有内核都在竞争相同的L2缓存。
为了充分利用它,C ++人员使用PGA(配置文件引导优化),允许他们分析他们的应用程序,该应用程序用作编译器的输入数据,以便为特定用例发出更好的优化代码。
在托管代码中,您可以在某种程度上获得更好的效果,但由于影响您的缓存局部性的因素很多,因此总体缓存局部性不太可能使您在现实世界中看到20的加速。这仍然是使用分析数据的C ++和编译器的制度。
答案 5 :(得分:1)
您可以从中获得一些想法:
Cache-Oblivious http://supertech.csail.mit.edu/cacheObliviousBTree.html Cache-Oblivious Search Trees Project
DSapce@MIT Cache coherence strategies in a many-core processor http://dspace.mit.edu/handle/1721.1/61276