如果这是一个模糊的问题,我深表歉意,但是这里有:
在过去的几年中,函数式编程在软件工程界引起了很多关注。许多人已经开始使用诸如Scala和Haskell之类的语言,并声称比其他编程语言和范例更成功。我的问题是:作为高性能计算/科学计算专家,我们应该对函数式编程感兴趣吗?我们应该参加这次小型革命吗?
函数式编程在SciComp工作领域中的优缺点是什么?
如果这是一个模糊的问题,我深表歉意,但是这里有:
在过去的几年中,函数式编程在软件工程界引起了很多关注。许多人已经开始使用诸如Scala和Haskell之类的语言,并声称比其他编程语言和范例更成功。我的问题是:作为高性能计算/科学计算专家,我们应该对函数式编程感兴趣吗?我们应该参加这次小型革命吗?
函数式编程在SciComp工作领域中的优缺点是什么?
Answers:
我只做了一点函数式编程,因此请一筹莫展。
我认为“缺点”部分中的许多反对意见都可以克服。作为此Stack Exchange网站上的常见讨论点,开发人员时间比执行时间更重要。即使功能性编程语言很慢,如果可以将性能关键的部分委派给较快的过程语言,并且可以通过快速的应用程序开发证明生产率的提高,那么它们可能值得使用。在这里值得注意的是,用纯Python,纯MATLAB和纯R实现的程序比用C,C ++或Fortran进行这些相同程序的实现要慢得多。诸如Python,MATLAB和R之类的语言之所以受欢迎,正是因为它们以执行速度为代价来换取生产力,即使如此,Python和MATLAB都具有用于实现与C或C ++的已编译代码的接口的功能,因此可以将对性能至关重要的代码实现为快速执行。大多数语言都具有C的外部函数接口,足以与计算科学家感兴趣的大多数库进行接口。
这一切都取决于您认为什么很酷。如果您是愿意打破常规的人,并且愿意向人们传福音,那就是您想对函数式编程做任何事情的好处,我会说的坚持下去。我很想看到人们用计算科学中的函数式编程做一些很棒的事情,只要没有其他理由证明所有反对者都是错误的(并且将会有很多反对者)。如果您不是那种想和一群人打招呼的人,“为什么在地狱中使用功能性编程语言而不是(在此处插入他们最喜欢的过程编程语言)?”,那么我不会麻烦了。
函数式编程语言已用于模拟密集型工作。定量交易公司Jane Street使用OCaml进行财务建模和执行其交易策略。OCaml还用于FFTW中,以生成库中使用的某些C代码。Liszt是斯坦福大学开发的领域特定语言,并在Scala中实现,用于解决PDE。函数式编程肯定会在工业中使用(不一定在计算科学中使用);它是否会在计算科学中起飞还有待观察。
我对此可能有独特的见解,因为我是具有科学计算背景并且是函数式编程语言用户的HPC从业人员。我不想将HPC等同于科学计算,但是存在很多交叉点,因此这就是我回答这一问题的观点。
目前,在HPC中不太可能采用功能语言,这主要是因为HPC用户和客户真正关心的是尽可能地接近峰值性能。的确,以功能性方式编写代码时,自然会暴露出可以利用的并行性,但在HPC中还不够。并行性只是实现高性能的难题之一,您还必须考虑到广泛的微体系结构细节,并且这样做通常需要对代码的执行进行非常细粒度的控制,而这种控制在任何情况下都不可用。我所知道的功能语言。
也就是说,我寄希望于这种情况可能会改变。我注意到一种趋势,研究人员开始意识到,许多微体系结构优化可以(在一定程度上)实现自动化。这孕育了源到源编译器技术的动物园,用户在其中输入他们想要进行的计算的“规范”,并且编译器输出C或Fortran代码,以实现有效地进行必要的优化和并行处理的计算使用目标架构。顺便说一下,这就是功能语言非常适合做的事情:建模和分析编程语言。函数语言的第一个主要采用者是编译器开发者,这绝非偶然。除了一些值得注意的例外,我还没有看到它真正成立,但是想法就在那里,
我想在其他两个答案中增加一个方面。除了生态系统,函数式编程为并行执行(例如多线程或分布式计算)提供了绝佳的机会。它固有的不变性使其适用于并行性,这在命令式语言中通常很难解决。
由于近年来硬件性能的提高一直侧重于向处理器添加内核,而不是推动更高的频率,因此并行计算正变得越来越流行(我敢打赌大家都知道这一点)。
Geoff提到的另一件事是开发人员时间通常比执行时间更重要。我在一家构建计算密集型SaaS的公司工作,刚开始时我们进行了初步的性能测试,将C ++与Java进行了对比。我们发现C ++比Java缩短了大约50%的执行时间(这是用于计算几何的,并且数字很可能会因应用程序而异),但是我们还是选择了Java,因为开发者时间的重要性,并希望优化和未来硬件性能的改进将有助于我们将其投放市场。我可以充满信心地说,如果我们选择其他方式,我们将不会继续做生意。
好的,但是Java不是一种功能编程语言,所以您可能会问它与任何东西有什么关系。好吧,后来,当我们使用功能范例的更多支持者并偶然发现并行化的需求时,我们逐步将系统的某些部分迁移到Scala,后者将函数式编程的积极方面与命令性功能结合在一起,并与Java。当以最小的麻烦提高系统性能时,它为我们提供了极大帮助,并且当更多的内核被塞入明天的处理器中时,它可能会继续从硬件业务的进一步性能提升中受益。
请注意,我完全同意其他答案中提到的缺点,但是我认为并行执行的便利性是如此强大,以至于不能一概而论。
杰夫(Geoff)已经很好地概述了我除了强调他的观点之一:生态系统之外没有多大理由的原因。无论您是提倡函数式编程还是任何其他范式,您必须解决的重要问题之一就是,您必须重写许多其他软件,而其他任何软件都可以构建。示例是用于线性代数的MPI,PETSc或Trilinos或任何有限元库-全部用C或C ++编写。系统中存在着很大的惯性,也许不是因为每个人都认为C / C ++实际上是编写计算软件的最佳语言,而是因为许多人花了很多年的时间来创建对很多人。
我认为大多数计算人员都会同意,尝试新的编程语言并评估其对这个问题的适用性具有很多价值。但这将是一个艰难而孤独的时期,因为您将无法产生与其他人所做的事情竞争的结果。它也可能使您声名远扬,成为开始着手另一种编程范例的人。嘿,用C ++大约花了15年的时间来替换Fortran!
快速总结是
这些事实使得大多数用户似乎不需要进行功能编程。
我认为有趣的是,在计算科学中使用函数式编程并不是新事物。例如,1990年的这篇论文展示了如何使用部分评估来提高用Lisp(可能是最早的函数式编程语言)编写的数字程序的性能。这项工作是GJ Sussman(SICP名望)和J Wisdom 在1992年的论文中使用的工具链的一部分,该工具链提供了太阳系混沌行为的数值证据。可以在此处找到有关该计算所涉及的硬件和软件的更多详细信息。
R是一种功能语言,也是一种统计(现在是机器学习)语言,实际上是统计的第一语言。它不是HPC语言:它不用于物理模拟等传统的“数字运算”。但是,可以使其在大型群集上运行(例如,通过MPI),以进行机器学习的大型统计模拟(MCMC)。
Mathematica也是一种功能语言,但它的核心领域是符号计算而不是数值计算。
在Julia中,您还可以采用功能样式(在过程及其面向对象的风格(多调度)之后)进行编程,但是它不是纯净的(基本数据结构都是可变的(元组除外),尽管有些库具有不可变的功能数据结构,更重要的是,它比过程样式要慢得多,因此使用不多。
我不会将Scala称为功能性语言,而是对象功能的混合体。您可以在Scala中使用许多功能概念。由于使用Spark(Spark ),Scala对云计算非常重要。(https://spark.apache.org/)。
请注意,现代Fortran实际上具有功能编程的一些元素:它具有严格的指针语义(与C不同),您可以具有纯函数(无副作用)(并标记为此类),并且可以具有不变性。它甚至具有智能索引功能,您可以在其中指定矩阵索引的条件。这类似于查询,通常只能在高级语言(如C#中的LINQ的R)中找到,或通过功能语言中的更高阶过滤器函数找到。因此,Fortran一点也不差劲,它甚至具有许多语言都没有的一些非常现代的功能(例如,协同数组)。实际上,在Fortran的未来版本中,我宁愿看到更多的功能性功能,而不是OO功能(现在通常是这种情况),因为Fortran中的OO确实笨拙且丑陋。
优点是每种功能语言中内置的“工具”:过滤数据非常容易,对数据进行迭代非常容易,并且为您的问题提供清晰简洁的解决方案非常容易。
唯一的缺点是,您必须开始思考这种新的思维方式:可能需要一些时间来了解自己必须知道的内容。SciComp域中的其他语言并未真正使用这些语言,这意味着您无法获得太多的支持:(
如果您对功能科学语言感兴趣,我开发了一个 https://ac1235.github.io
这是我为什么函数式编程可以并且应该用于计算科学的观点。好处是巨大的,缺点很快就会消失。在我看来,只有一个缺点:
精读:缺乏C / C ++ / Fortran的语言支持
至少在C ++中,这种缺点正在消失-因为C ++ 14/17添加了强大的功能来支持函数式编程。您可能需要自己编写一些库/支持代码,但是该语言将是您的朋友。例如,这是一个(警告:插件)库,它在C ++中执行不变的多维数组:https : //github.com/jzrake/ndarray-v2。
另外,这是一本有关C ++函数式编程的好书的链接,尽管它并不专注于科学应用。
这是我认为是专业人士的总结:
优点:
就正确性而言,功能程序的位置很明确:它们会迫使您正确定义物理变量的最小状态,以及使该状态随时间前进的功能:
int main()
{
auto state = initial_condition();
while (should_continue(state))
{
state = advance(state);
side_effects(state);
}
return 0;
}
求解偏微分方程(或ODE)非常适合函数式编程。您只需将一个纯函数(advance
)应用于当前解决方案即可生成下一个函数。
以我的经验,物理模拟软件总会受到状态管理不佳的困扰。通常,算法的每个阶段都在某个共享(有效全局)状态中运行。这使得很难或什至不可能确保正确的操作顺序,使软件容易受到可能表现为段错误的错误的影响,或更糟糕的是,错误术语不会使您的代码崩溃,但会无形中损害其科学的完整性。输出。在物理模拟中尝试管理共享状态的尝试也会抑制多线程处理,这是未来的问题,因为超级计算机正朝着更高的内核数量发展,而使用MPI进行扩展通常会达到约10万个任务。相反,由于不变性,函数式编程使共享内存并行性变得微不足道。
由于对算法进行了惰性评估,因此函数式编程的性能也得到了提高(在C ++中,这意味着在编译时生成许多类型-通常针对函数的每个应用程序生成一种类型)。但是,它减少了内存访问和分配的开销,并且消除了虚拟调度-允许编译器通过一次查看构成该算法的所有功能对象来优化整个算法。在实践中,您将试验评估点的不同排列方式(将算法结果缓存到内存缓冲区中),以优化CPU的使用与内存分配。与通常在模块或基于类的代码中看到的情况相比,由于算法阶段的局部性很高(请参见下面的示例),因此这相当容易。
只要使物理状态变得琐碎,功能程序就更容易理解。这并不是说您的所有同事都容易理解它们的语法!作者应谨慎使用命名功能,并且研究人员通常应习惯于看到功能上而非程序上表达的算法。我承认,缺乏控制结构可能会使某些人感到反感,但我不认为这会阻止我们进入能够在计算机上进行更优质科学的未来。
下面是一个示例advance
函数,使用该ndarray-v2
程序包从有限体积的代码改编而成。注意to_shared
操作员-这些是我之前提到的评估点。
auto advance(const solution_state_t& state)
{
auto dt = determine_time_step_size(state);
auto du = state.u
| divide(state.vertices | volume_from_vertices)
| nd::map(recover_primitive)
| extrapolate_boundary_on_axis(0)
| nd::to_shared()
| compute_intercell_flux(0)
| nd::to_shared()
| nd::difference_on_axis(0)
| nd::multiply(-dt * mara::make_area(1.0));
return solution_state_t {
state.time + dt,
state.iteration + 1,
state.vertices,
state.u + du | nd::to_shared() };
}