有什么办法可以优化 Ruby 中这个关键的循环?

3
我正在使用Ruby编写一个CSV库(我知道,标准库已经很好了!)主要是为了好玩。目前它的速度大约比标准库慢4倍,我发现这很奇怪,因为我查看了stdlib中的csv.rb,它使用正则表达式来分割行,我认为这不会很快。在我的库中,我使用了一个DFA,所以我确定它将在O(n)时间内运行 - 我几乎没有回溯,只有一种情况我需要回溯一次,以适应反常的情况(转义字符==引号字符),而且这种情况只发生了大约1%的时间。
显然,我对我的代码进行了性能分析,这里是总运行时间占89%的部分。这是每个输入文件字符都要运行的循环:
def consume token
  if !@separator and [:BEFORE_FIELD, :FIELD, :BEFORE_SEPARATOR].include?(@state)
    if @potential_separators.include? token
      @separator = token
    end
  end

  #puts "#{@state} - Token: #{token}"
  @state = case @state
  when :QUOTED_FIELD
    if @escape.include? token
      @last_escape_used = token
      :MAYBE_ESCAPED_QUOTE
    elsif token == @quote
      :BEFORE_SEPARATOR
    else
      @field += token
      :QUOTED_FIELD
    end
  when :FIELD
    case token
    when @newline
      got_field
      got_row
      :BEFORE_FIELD
    when @separator
      got_field
      :BEFORE_FIELD
    else
      @field += token
      :FIELD
    end
  when :BEFORE_FIELD
    case token
    when @separator
      got_field
      :BEFORE_FIELD
    when @quote
      :QUOTED_FIELD
    when @newline
      got_field
      got_row
      :BEFORE_FIELD
    else
      @field += token
      :FIELD
    end
  when :MAYBE_ESCAPED_QUOTE
    if token == @quote
      @field += @quote
      :QUOTED_FIELD
    elsif @last_escape_used == @quote
      @state = :BEFORE_SEPARATOR
      consume token
    else
      @field += @last_escape_used
      @field += token
      :QUOTED_FIELD
    end
  when :BEFORE_SEPARATOR
    case token
    when @separator
      got_field
      :BEFORE_FIELD
    when @newline
      got_field
      got_row
      :BEFORE_FIELD
    else
      raise "Error: Separator or newline expected! Got: #{token} at (#{@line}:#{@column})"
    end
  end

  if token == @newline
    @column =  1
    @line   += 1
  else
    @column += token.length
  end
  #puts "[#{@line}:#{@column} - #{token}] Switched to #{@state}"
  #if token == @quote then exit end
  @state
end

以下是性能分析输出:

KCacheGrind profiling output

此外,真正缓慢的是consume函数本身,而不是它调用的少数函数,因为Self部分占总运行时间的62%!

#consume函数发生的详细信息如下:

KCacheGrind profiling details

我认为case @state可能是罪魁祸首,所以我首先做的事情就是将最频繁的case放在前面(并进行了基准测试:没有变化)。代码看起来很干净,我真的不知道在哪里可以获得更多优化,但我觉得奇怪的是它比标准库慢这么多。

顺便说一下,我正在对一个2MB的CSV文件进行测试。我逐行读取它,并在内存中不存储任何内容。如果我用一个什么也不做的函数替换我的consume函数,那么我会得到与Ruby标准CSV相同的速度,但当然它什么也不做,所以我认为瓶颈不在I/O中。


你的方法是否使用了大量的if/else或其他分支语句,相比标准库而言?正则表达式可能看起来很慢,但防止CPU使用分支预测可能会产生相当负面的影响。 - Chris Hayes
大多数是我在这里粘贴的代码中的那些,有一堆但我认为没有什么奢侈的东西。 - djfm
对于非病态的正则表达式,我期望正则引擎非常快,因为它们是本地化的且经过了强制调整。请注意,许多使用DFA和本地一种算法。您可能会对这篇文章感兴趣,甚至更感兴趣的是它所引用的三篇文章:有趣的东西(对于“有趣”更极客的价值观)。 - Dave Newton
那个方法太大了。为了保持清晰,将其拆分成更小、更完整的方法。这可能会稍微减慢一点速度(由于函数调用的开销),但它使代码更容易理解。 - Andrew Marshall
正确编写的正则表达式非常快。我在 Stack Overflow 上进行了许多基准测试,将它们与其他算法进行比较,除了非常特定的用途外,良好编写的正则表达式将获胜。话虽如此,一个编写不当的正则表达式可能会严重浪费 CPU 时间,并且难以维护。 - the Tin Man
1个回答

0

两点:

  • 你似乎有很多some_array.include?(something)。这很慢。尝试用哈希表替换它,像这样:

    some_hash = {this_key => true, that_key => true, ...}

    并使用some_hash[something]

  • 你似乎有很多some_string += another_string。这很慢。尝试改为:

    some_string << another_string


谢谢,将+ =替换为<<的效果略有提升(从1600ms降至1400ms!)。至于array.include?它们实际上是集合,除了一个之外,很少被调用。 - djfm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接