【问题标题】:Are functional languages inherently slow? [closed]函数式语言天生就慢吗? [关闭]
【发布时间】:2010-10-05 16:52:23
【问题描述】:

为什么函数式语言在基准测试中总是落后于 C?如果您有静态类型的函数式语言,在我看来,它可以编译为与 C 相同的代码,或者甚至更优化的代码,因为编译器可以使用更多语义。为什么看起来所有函数式语言都比 C 慢,为什么它们总是需要垃圾收集和过度使用堆?

有谁知道适合嵌入式/实时应用程序的函数式语言,其中内存分配保持在最低限度并且生成的机器代码精简且快速?

【问题讨论】:

  • 多线程函数式语言应用程序会比单线程 C 应用程序快还是慢?我认为这是最重要的问题。
  • @tuinstoel 当然取决于(尽管您可能暗示它会更快)。并行化在启动线程等方面有其自身的成本。一般来说,并行化在一定程度上是有回报的。在此之前,单线程更快。之后,并行化速度更快。作为一个例子,看看 SlavaVendenin 的回答:stackoverflow.com/questions/309424/…。如您所见,Java 的并行化流比具有足够小循环的 for 循环慢得多。

标签: compilation functional-programming


【解决方案1】:

没有什么是天生的。这是interpreted OCaml runs faster than equivalent C code 的示例,因为由于语言的差异,OCaml 优化器具有不同的可用信息。当然,笼统地声称 OCaml 绝对比 C 快是愚蠢的。关键是,这取决于你在做什么,以及你是如何做的。

也就是说,OCaml 是一个(大部分)函数式语言的例子,它实际上是为performance 设计的,与纯度相反。

【讨论】:

  • 另一个显示 OCaml 与 C 一样快的基准测试:timestretch.com/FractalBenchmark.html
  • 让我们清楚一点,ocaml 编译器(甚至 opt)是一个本机代码编译器,而不是优化器。
  • 我不明白 C 代码为什么会比 C 程序解释的 OCaml 字节码慢,因为后者肯定会遇到完全相同的别名问题?!
  • @JonHarrop:这个答案已有 7 年历史了(!),但我确实修复了第一个链接,但它不再起作用了。
【解决方案2】:

函数式语言天生就慢吗?

在某种意义上,是的。他们需要的基础设施不可避免地增加了理论上可以使用手工汇编程序实现的开销。特别是,一流的词法闭包只适用于垃圾回收,因为它们允许值超出范围。

为什么函数式语言在基准测试中总是落后于 C?

首先,谨防选择偏差。 C 作为基准套件中的最低公分母,限制了可以完成的工作。如果你有一个比较 C 和函数式语言的基准,那么它几乎可以肯定是一个非常简单的程序。可以说如此简单,以至于在今天几乎没有实际意义。仅使用 C 作为基准来解决更复杂的问题实际上是不可行的。

最明显的例子是并行性。今天,我们都有多核。甚至我的手机也是多核的。众所周知,多核并行在 C 语言中很困难,但在函数式语言中却很容易(我喜欢 F#)。其他示例包括任何受益于持久数据结构的东西,例如撤消缓冲区对于纯函数式数据结构来说是微不足道的,但在 C 等命令式语言中可能需要大量工作。

为什么看起来所有函数式语言都比 C 慢,为什么它们总是需要垃圾收集和过度使用堆?

函数式语言看起来会更慢,因为您只会看到比较容易用 C 编写好的代码的基准测试,而您永远不会看到比较函数式语言开始表现出色的更复杂任务的基准测试。

但是,您已经正确地确定了当今函数式语言的最大瓶颈可能是什么:它们的分配率过高。干得好!

函数式语言分配如此繁重的原因可以分为历史原因和固有原因。

从历史上看,Lisp 实现已经进行了 50 年的大量拳击。这种特性传播到许多其他使用类似 Lisp 的中间表示的语言。多年来,语言实施者一直在使用拳击作为解决语言实施复杂性的快速解决方案。在面向对象的语言中,默认设置总是堆分配每个对象,即使它显然可以被堆栈分配。然后效率的负担被推到了垃圾收集器上,并且已经投入了大量精力来构建垃圾收集器,这些垃圾收集器可以达到接近堆栈分配的性能,通常是通过使用凹凸分配的 Nursery 生成。我认为应该投入更多的精力来研究功能语言设计,以最大限度地减少装箱和垃圾收集器设计,这些设计针对不同的需求进行了优化。

通用垃圾收集器非常适合堆分配大量的语言,因为它们几乎可以与堆栈分配一样快。但它们在其他地方增加了大量的间接费用。今天的程序越来越多地使用队列之类的数据结构(例如用于并发编程),这些数据结构给分代垃圾收集器带来了病态的行为。如果队列中的项目超过第一代,那么它们都被标记,然后它们都被复制(“撤离”),然后对其旧位置的所有引用都得到更新,然后它们就有资格被收集。这比它需要的速度慢了大约 3 倍(例如,与 C 相比)。像Beltway(2002)和Immix(2008)这样的标记区域收集器有可能解决这个问题,因为托儿所被一个可以像托儿所一样被收集的区域所取代,或者如果它包含大部分可达值,它可以被另一个区域替换并保持老化,直到它包含大部分无法访问的值。

尽管 C++ 已经存在,但 Java 的创建者错误地为泛型采用了类型擦除,从而导致了不必要的装箱。例如,I benchmarked a simple hash table running 17× faster on .NET than the JVM 部分是因为 .NET 没有犯这个错误(它使用了具体化的泛型),也因为 .NET 具有值类型。我实际上责怪 Lisp 让 Java 变慢了。

所有现代函数式语言实现都继续过度装箱。 Clojure 和 Scala 等基于 JVM 的语言几乎没有选择余地,因为它们所针对的 VM 甚至无法表达值类型。 OCaml 在其编译过程的早期就丢弃类型信息,并在运行时使用标记整数和装箱来处理多态性。因此,OCaml 经常将单个浮点数装箱并且总是将元组装箱。例如,OCaml 中的三个字节由一个指针(其中嵌入了一个隐式的 1 位标记,在运行时重复检查)表示,该指针指向一个堆分配的块,该块具有 64 位标头和 192 位主体,其中包含三个标记的 63 位整数(其中 3 个标记再次在运行时重复检查!)。这显然是疯了。

在函数式语言的拆箱优化方面已经做了一些工作,但从未真正获得关注。例如,标准 ML 的 MLton 编译器是一个全程序优化编译器,它进行了复杂的拆箱优化。可悲的是,它早于它的时代,而且“长”的编译时间(在现代机器上可能不到 1 秒!)阻止了人们使用它。

唯一打破这一趋势的主要平台是 .NET,但令人惊讶的是,这似乎是个意外。尽管有一个 Dictionary 实现对值类型的键和值进行了非常优化(因为它们是未装箱的),但像 Eric Lippert 这样的 Microsoft 员工继续使用 claim that the important thing about value types is their pass-by-value semantics 而不是源自其未装箱内部表示的性能特征。 Eric 似乎被证明是错误的:更多的 .NET 开发人员似乎更关心拆箱而不是按值传递。实际上,大多数结构是不可变的,因此是引用透明的,因此按值传递和按引用传递之间没有语义差异。性能是可见的,结构可以提供巨大的性能改进。 structs 的性能甚至节省了 Stack Overflow 和 structs 用于避免商业软件中的 GC 延迟,如Rapid Addition's

函数式语言分配繁重的另一个原因是固有的。像哈希表这样的命令式数据结构在内部使用巨大的整体数组。如果这些是持久的,那么每次进行更新时都需要复制巨大的内部数组。因此,像平衡二叉树这样的纯功能数据结构被分割成许多小的堆分配块,以便于从一个版本的集合到下一个版本的重用。

当字典之类的集合仅在初始化期间写入然后大量读取时,Clojure 使用了一个巧妙的技巧来缓解这个问题。在这种情况下,初始化可以使用变异来构建“幕后”结构。但是,这对增量更新没有帮助,并且生成的集合仍然比它们的命令式等价物读起来慢得多。从好的方面来说,纯函数式数据结构提供持久性,而命令式数据结构则没有。然而,很少有实际应用程序从实践中的持久性中受益,因此这通常不是有利的。因此,人们渴望使用不纯的函数式语言,您可以轻松地使用命令式风格并从中获益。

有谁知道适合嵌入式/实时应用程序的函数式语言,其中内存分配保持在最低限度并且生成的机器代码精简且快速?

如果您还没有,请查看 Erlang 和 OCaml。两者对于内存受限的系统都是合理的,但都不会生成特别好的机器代码。

【讨论】:

  • “所有现代函数式语言实现都继续过度装箱”但是为什么呢?关闭是一回事,更多的原因?我在哪里可以了解更多信息?
  • “如果这些是持久的,那么每次更新时都需要复制巨大的内部数组”。持久数据结构的想法不是不需要复制,更新可以引用原始结构吗?
  • “持久数据结构的想法不是不需要复制,而是更新可以引用原始结构”。是的,这个想法是新版本可以引用旧版本的部分内容,但这需要将大型数组分割成许多较小的数据结构(部分),以便能够引用这些部分。
  • “为什么很少有实际应用程序从实践中的持久性中受益”。实际应用程序很少需要一次保留多个版本的数据结构。所以主要的好处是清晰。
  • 好帖子!我真的很想阅读您关于 2020 年函数式语言编译器状态的帖子。 OCaml 在拳击方面有改进吗?
【解决方案3】:

Haskell 仅比 GCC 的 C++ 慢 1.8 倍,后者在典型的基准测试任务中比 GCC 的 C 实现要快。 这使得 Haskell 非常快,甚至比 C#(即 Mono)还要快。

相对语言 速度

  • 1.0 C++ GNU g++
  • 1.1 C GNU gcc
  • 1.2 ATS
  • 1.5 Java 6-服务器
  • 1.5 干净
  • 1.6 无帕斯卡帕斯卡
  • 1.6 Fortran 英特尔
  • 1.8 Haskell GHC
  • 2.0 C# 单声道
  • 2.1 斯卡拉
  • 2.2 Ada 2005 GNAT
  • 2.4 Lisp SBCL
  • 3.9 Lua LuaJIT

source

作为记录,我在 iPhone 上使用 Lua 进行游戏,因此如果您愿意,可以轻松使用 Haskell 或 Lisp,因为它们更快。

【讨论】:

  • 在该列表中,领先于 Haskell 的是另一种纯函数式语言 - Clean。
  • 没有研究过 Clean,但听说它是另一种函数式语言很有趣
  • 你忘了提到 PHP ;) 因子 25 左右 ;)
  • -1 根据任何合理的定义,其中大部分都不是“Haskell”代码。
  • 嗨罗伯特,链接已失效
【解决方案4】:

更大的可执行文件大小的另一个原因可能是惰性评估和非严格性。编译器无法在编译时确定某些表达式何时被评估,因此一些运行时被填充到可执行文件中来处理这个问题(调用所谓的 thunks 的评估)。至于性能,懒惰既好也坏。一方面它允许额外的潜在优化,另一方面代码大小可以更大,程序员更有可能做出错误的决定,例如参见 Haskell 的 foldl vs. foldr vs. foldl' vs. foldr'

【讨论】:

    【解决方案5】:

    C 速度很快,因为它基本上是一组用于汇编程序的宏 :) 当你用 C 编写程序时,没有“幕后”。当你决定是时候分配内存并释放同样的时尚。当您编写实时应用程序时,这是一个巨大的优势,其中可预测性很重要(实际上比其他任何事情都重要)。

    此外,C 编译器通常非常快,因为语言本身很简单。它甚至不进行任何类型检查:) 这也意味着更容易发现错误。 缺少类型检查的广告优势在于,函数名称可以直接与其名称一起导出,这使得 C 代码很容易与其他语言的代码链接

    【讨论】:

    • -1 “用 C 编写程序时没有“幕后”。调用约定、寄存器和内存模型是明显的反例。
    • @Jon Harrop:很公平。我在写那行时正在考虑内存管理(如下句所示),但确实我的表达具有误导性
    • C 中的内存分配通常通过对 C 运行时的 mallocfree 调用完成,这在汇编程序中不是惯用的。
    • 是的,我认为值得强调的是mallocfree 自己在幕后做了很多工作。我经常听到人们在延迟的情况下摒弃垃圾收集语言,转而支持 C,但他们似乎从来不知道 mallocfree 导致的延迟......
    • @happy_emi:我从来没有认为 C 编译器“非常快”。事实上,我通常觉得 C 的编译时间很可悲,对于 C++ 来说简直就是糟糕透顶。一个主要原因是他们分离编译的过时方法。
    【解决方案6】:

    过程语言的控制流更符合现代计算机的实际处理模式。

    C 非常紧密地映射到其编译产生的汇编代码,因此有“跨平台汇编”的绰号。计算机制造商花了几十年的时间让汇编代码尽可能快地运行,因此 C 继承了所有这些原始速度。

    相比之下,函数式语言的无副作用、固有的并行性根本不能很好地映射到单个处理器上。调用函数的任意顺序需要被序列化到 CPU 瓶颈:如果没有非常聪明的编译,您将一直在进行上下文切换所有,任何预取都不会工作是因为你经常到处乱跳,......基本上,计算机制造商为好的、可预测的过程语言所做的所有优化工作几乎都是无用的。

    但是!随着向许多功能较弱的内核(而不是一两个涡轮增压内核)发展,函数式语言应该开始缩小差距,因为它们自然地水平扩展。

    【讨论】:

    • +1 为什么内核必须不那么强大?为什么不简单地增加与当前核心相同或更大功率的核心?
    • 他们没有必须变得不那么强大,只是因为芯片设计人员专注于将更多内核放在芯片上,并让内核和 CPU 在 SMP 中运行良好在这种情况下,他们不太关注原始速度。不幸的是,他们不能专注于所有事情! :)
    • -1 “过程语言的控制流更符合现代计算机的实际处理模式”。编译器越来越多地将变异转换为单个静态赋值。
    • “C 实际上比函数式语言更匹配汇编”
    • 您应该包括一个事实,即对于更简单、更小或(当然)不可并行化的任务,单线程通常更快。坦率地说,很多很多人都没有做任何大到可并行化的事情来获得并行化的好处。例如,在此处查看 Java 的并行化流与简单 for 循环的性能:stackoverflow.com/questions/309424/…。有人在那里做了一个有趣的基准测试。
    【解决方案7】:

    我不同意 tuinstoel。重要的问题是,当函数式语言用于本应使用的函数式语言时,它是否能提供更快的开发时间并产生更快的代码。请参阅efficiency issues section on Wikipedia 了解我的意思。

    【讨论】:

      【解决方案8】:

      函数式语言需要消除在语言抽象级别可见的可变状态。因此,需要复制将由命令式语言就地突变的数据,而突变发生在副本上。举个简单的例子,请参阅 Haskell 与 C 中的快速排序。

      此外,垃圾收集是必需的,因为free() 不是纯函数,因为它有副作用。因此,在语言抽象级别释放内存且不涉及副作用的唯一方法是垃圾回收。

      当然,原则上,足够智能的编译器可以优化大部分复制。这在某种程度上已经完成了,但是要让编译器足够智能以理解该级别代码的语义是非常困难的。

      【讨论】:

      • “功能语言需要消除在语言抽象级别可见的可变状态”。大多数函数式语言都是不纯的。
      • 而那些纯的只包含副作用,不会去除它们。例如,Haskell 会产生很多副作用,包括异常、io、打印、线程,但这些操作被封装为操作流而不是 unsafePerformIO。整个想法是注意副作用并密切关注它们。可以在 io monad 中进行显式分配和解除分配。纯语言可以轻松发射导弹并且仍然是纯语言,函数式范式在定义纯洁性方面非常聪明。
      【解决方案9】:

      就目前而言,函数式语言并没有大量用于行业项目,因此没有足够的认真工作投入到优化器中。此外,为命令式目标优化命令式代码可能更容易。

      函数式语言有一个壮举可以让它们很快超越命令式语言:微不足道的并行化。

      微不足道不是因为它很容易,而是它可以内置到语言环境中,而无需开发人员考虑。

      在像 C 这样的与线程无关的语言中,强大的多线程的成本对于许多项目来说是令人望而却步的。

      【讨论】:

      • +1 提到并行化要容易得多(由编译器手动和自动)
      • “函数式语言没有大量用于行业项目”。 C# 具有一流的词法闭包。
      • “函数式语言有一项壮举可以让它们很快超越命令式语言”。 FWIW 我认为这永远不会发生。纯度使参考的局部性变得不可预测,而多核并行性需要局部性才能进行扩展。
      • @JonHarrop “纯度使参考位置不可预测”您能否详细说明一下或提供链接?
      • @lxx:当然,有效的多核并行性需要有效地使用机器的缓存层次结构。在命令式风格中,您使用数组并特别注意显式局部性,以实现良好的缓存复杂性(请参阅 Cilk 论文fftw.org/~athena/papers/tocs08.pdf)。在纯函数式风格中,您不知道您的值在内存中的位置,也无法预测缓存复杂性,因此并行可扩展性是不可预测的:有时您很幸运,但它并不可靠。
      【解决方案10】:

      简短的回答:因为 C 很快。就像,快得快得可笑的疯狂。一种语言根本不必“慢”就可以被 C 掌握。

      C 之所以这么快,是因为它是由非常出色的编码人员创建的,而 gcc 已经经过数十年的优化,并且由数十位优秀的编码人员进行了优化,超过了 99% 的语言。

      简而言之,除了需要非常特殊的函数式编程结构的特殊任务之外,您不会击败 C。

      【讨论】:

      • 在我看来,C 速度很快,因为它做的不多(作为一种语言)。它让你编写所有细节,并且不会做任何你没有明确告诉它去做的事情。
      • 不知道为什么这个被标记了。不完全是学术答案,但仍然有效。
      • 这说明“高层大会”这个词组是根据 C 创造的。
      • -1 C 实际上在没有优化的情况下也很快。有一些简单的 C 编译器(例如 TCC = tiny c 编译器)没有经过“数十个更出色的编码器”进行优化而不是表现得相当好。
      • C++ 通常比 C 快得多。
      猜你喜欢
      • 2011-01-13
      • 2011-09-04
      • 2011-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多