【问题标题】:Why is this Elixir code so slow?为什么这个 Elixir 代码这么慢?
【发布时间】:2015-07-14 18:35:45
【问题描述】:

我目前对学习 Elixir 非常感兴趣,我学习一门新语言的典型方法是用它构建简单的程序。

所以我决定编写一个(非常简单的)类似 grep 的程序(或模块),如下所示:

defmodule LineMatch do
  def file(s, path) do
    case File.open(path, [:read]) do
      {:ok, fd} -> match s, fd
      {_, error} -> IO.puts "#{:file.format_error error}"
    end
  end
  defp match(s, fd) do
    case IO.read fd, :line do
      {:error, error} -> IO.puts("oh noez! Error: #{error}")
      line -> match(s, line, fd)
    end
  end
  defp match(s, line, fd) when line !== :eof do
    if String.contains?(line, s) do
      IO.write line
    end
    match(s, IO.read(fd, :line), fd)
  end
  defp match(_, _line, _) when _line === :eof do
  end
end

这很可能不是最优雅的方式,而且我对函数式编程也很陌生,所以我没想到它会超级快。但它不仅不快,而且实际上是超级慢。太慢了,我可能犯了一些非常明显的错误。

谁能告诉我,它是什么以及如何使它变得更好?

我通常使用单独的 .exs 文件来测试代码,例如

case System.argv do
  [searchTerm, path] -> LineMatch.file(searchTerm, path)
  _ -> IO.puts "Usage: lineMatch searchTerm path"
end

【问题讨论】:

    标签: elixir


    【解决方案1】:

    您可以通过做两件事来获得良好的性能,而不是像 lad2025 的回答那样读取整个文件。首先,使用IO.binstream 构建文件行的流,但作为原始二进制文件(用于性能)。使用 IO.stream 读取为 UTF-8,因此在读取文件时会产生额外的转换成本。如果您需要 UTF-8 转换,那么它会很慢。此外,使用 Stream.filter/2Stream.map/2 函数应用过滤和映射操作可防止您多次迭代行。

    defmodule Grepper do
      def grep(path, str) do
        case File.open(path) do
          {:error, reason} -> IO.puts "Error grepping #{path}: #{reason}"
          {:ok, file} ->
            IO.binstream(file, :line)
            |> Stream.filter(&(String.contains?(&1, str)))
            |> Stream.map(&(IO.puts(IO.ANSI.green <> &1 <> IO.ANSI.reset)))
            |> Stream.run
        end
      end
    end
    

    我怀疑您的代码最大的问题是 UTF-8 转换,但可能是通过调用 IO.read 从文件中逐行“拉取”,而不是将行“推送”到您的过滤/printing 操作使用IO.stream|binstream,你会产生一些额外的费用。我必须查看 Elixir 的源代码才能确定,但​​是上面的代码在我的机器上性能非常好(我正在从 Olson 时区数据库中搜索一个 143kb 的文件,不知道它将如何在非常大的文件上执行我手头没有很好的示例文件)。

    【讨论】:

    • 感谢您非常详细的回答!我一定会尝试你的建议。我使用generatedata.com 生成示例数据。我仍然想知道为什么它那么慢。当然,UTF-8 转换会减慢它的速度,但我得到的输出是每 500 毫秒一行。
    • 我真的怀疑这是 UTF-8 转换。通过 File.read 处理文件非常慢,因为每个 I/O 事务都是 elixir 中进程之间的消息。读取一大块然后解析单个二进制文件几乎总是更快。
    【解决方案2】:

    使用 File.stream!会更有效率。 试试看:

    defmodule Grepper do
      def grep(path, str) do
        File.stream!(path)
          |> Stream.filter(&(String.contains?(&1, str)))
          |> Stream.map(&(IO.puts(IO.ANSI.green <> &1 <> IO.ANSI.reset)))
          |> Stream.run
      end
    end
    

    【讨论】:

    • 目前最快。 :timer.tc 表示 2600 到 3000 毫秒,而来自 bitwalker 的 binstream 变体需要 6400 到 6900 毫秒。现在我将使用 utf-8 转换,看看这是否是最初缓慢的根源。
    • 所以使用 File.steam!(path, [:utf8]) 根本没有改变速度。我仍然不明白为什么我做的这么慢。我看到这更慢,是的。但是每行输出之间的速度慢到 500 毫秒?
    • 这真的很奇怪,因为IO.streamFile.stream 有效地做同样的事情(IO.stream 被抽象为使用任何 IO 输入,但读取流的实现基本相同)。奇怪的是罗曼的解决方案和我的解决方案之间会有如此大的差异,但我必须牢记这一点。 File.stream! 没有 :utf8 模式,但你必须通过 :raw 来模拟我的 binstream 实现,因为 File.stream! 默认情况下会进行 UTF-8 转换。
    • 我在我的 900MB 测试数据集上尝试了新的实现,但速度仍然非常慢。虽然我最初的实现花了 39 秒,但 File.stream(_, [:raw]) 版本仍然需要 12 秒(没有 :raw,它只需要 1-2 秒,但 IO.binstream 非常慢,也需要 38 秒) .在命令行中使用原始的grep需要半秒钟。
    • 我认为,将这种简单的算法与原始 grep 进行比较是不正确的。您可以查看grep source code 并确保它实现了完全不同的算法。因此,为了获得可比较的结果,我们必须在 Elixir 中重新实现相同的算法。
    猜你喜欢
    • 2017-01-10
    • 2011-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-28
    • 2011-04-24
    • 1970-01-01
    • 2010-10-19
    相关资源
    最近更新 更多