1

我目前正在对 ruby​​ 进行一些速度测试,我需要将一些文本文件解析为数值。由于速度很慢,我想知道我的代码是否可以优化,或者 ruby​​ 是否真的那么慢。正在从文件中读取代码,这些文件包含大约 1 000 000 条随机生成的行或数字,我将只显示几行,以便您知道正在读取的内容。我需要读取的文件名作为参数传递,coed 是单独的脚本(只是为了我自己的清楚)。

首先我想解析一个简单的数字,输入格式如下:

type
number

type
number

...

我是这样做的:

incr = 1

File.open(ARGV[0], "r").each_line do |line|
  incr += 1
  if incr % 3 == 0
    line.to_i
  end

end

其次,我需要解析为一个列表,输入格式如下:

type
(1,2,3,...)

type
(1,2,3,...)

...

我就是这样做的

incr = 1

File.open(ARGV[0], "r").each_line do |line|
  incr += 1
  if incr % 3 == 0
    line.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}
  end

end

最后我需要解析为列表列表,输入格式如下:

type
((1,2,3,...),(1,2,3,...),(...))

type
((1,2,3,...),(1,2,3,...),(...))

...

我是这样做的:

incr = 1

File.open(ARGV[0], "r").each_line do |line|
  incr += 1
  if incr % 3 == 0
    line.split("),(").map{ |s| s.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}}

  end

end

我不需要显示任何结果,我只是在测速,所以不需要输出。我确实检查了结果,并且代码本身似乎工作正常,它们的速度非常慢,我想用 ruby​​ 提供的最佳速度进行测试。我知道有几个我可以使用的速度测试,但为了我的目的,我需要建立自己的。

我能做些什么更好?如何优化这段代码?我哪里出错了,或者这已经是红宝石可以做的最好的了?提前感谢您的提示和想法。

4

2 回答 2

3

在第一个中,而不是:

File.open(ARGV[0], "r").each_line do |line|

利用:

File.foreach(ARGV[0]) do |line|

而不是:

  incr += 1
  if incr % 3 == 0

利用:

 if $. % 3 == 0

$.是最后读取行的行号的魔术变量。

在第二个中,而不是:

line.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}

利用:

line.tr('()', '').split(',').map(&:to_i)

在第三个中,而不是:

line.split("),(").map{ |s| s.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}}

利用:

line.scan(/(?:\d+,?)+/).map{ |s| s.split(',', 0).map(&:to_i) }

以下是该行的工作方式:

line.scan(/(?:\d+,?)+/)
=> ["1,2,3,", "1,2,3,"]

line.scan(/(?:\d+,?)+/).map{ |s| s.split(',',0) }
=> [["1", "2", "3"], ["1", "2", "3"]]

line.scan(/(?:\d+,?)+/).map{ |s| s.split(',', 0).map(&:to_i) }
=> [[1, 2, 3], [1, 2, 3]]

我没有运行任何基准来比较速度,但更改也应该更快,因为gsub调用已经消失。我所做的更改不一定是最快的做事方式,它们是您自己的代码的更优化版本。

尝试将 Ruby 的速度与其他语言进行比较需要了解完成每个步骤的最快方法,基于该步骤的多个基准。这也意味着您在相同的硬件和操作系统上运行,并且您的语言都被编译为最高效的速度形式。语言在内存使用与速度之间进行权衡,因此,虽然一种语言可能比另一种慢,但它也可能更节省内存。

另外,在生产环境中编码时,必须将生成正确工作的代码的时间考虑到“哪个更快”等式中。C 非常快,但是对于大多数问题来说,编写程序比 Ruby 需要更长的时间,因为 C 不像 Ruby 那样握住你的手。当 C 代码需要一周的时间来编写和调试时,与需要一个小时的 Ruby 代码相比,哪个更快?只是要考虑的事情。


在我完成之前,我没有通读@tadman 的答案和评论。使用:

map(&:to_i)

曾经慢于:

map{ |s| s.to_i }

速度差异取决于您运行的 Ruby 版本。最初使用&:是在一些猴子补丁中实现的,但现在它已内置到 Ruby 中。当他们做出改变时,它加快了很多:

require 'benchmark'

foo = [*('1'..'1000')] * 1000
puts foo.size

N = 10
puts "N=#{N}"

puts RUBY_VERSION
puts

Benchmark.bm(6) do |x|
  x.report('&:to_i') { N.times { foo.map(&:to_i) }}
  x.report('to_i') { N.times { foo.map{ |s| s.to_i } }}
end

哪个输出:

1000000
N=10
2.0.0

             user     system      total        real
&:to_i   1.240000   0.000000   1.240000 (  1.250948)
to_i     1.400000   0.000000   1.400000 (  1.410763)

这要经过 10,000,000 个元素,仅导致 0.2/秒的差异。做同一件事的两种方式之间并没有太大区别。如果您要处理更多数据,那么这很重要。对于大多数应用程序来说,这是一个没有实际意义的问题,因为其他事情将成为瓶颈/减速,所以无论哪种方式适合您编写代码,都要牢记速度差异。


为了展示 Ruby 版本的不同之处,下面是使用 Ruby 1.8.7 的相同基准测试结果:

1000000
N=10
1.8.7

            用户系统总真实
&:to_i 4.940000 0.000000 4.940000 (4.945604)
to_i 2.390000 0.000000 2.390000 (2.396693)

至于gsubtr

require 'benchmark'

foo = '()' * 500000
puts foo.size

N = 10
puts "N=#{N}"

puts RUBY_VERSION
puts

Benchmark.bm(6) do |x|
  x.report('tr') { N.times { foo.tr('()', '') }}
  x.report('gsub') { N.times { foo.gsub(/[()]/, '') }}
end

有了这些结果:

1000000
N=10
1.8.7

            用户系统总真实
tr 0.010000 0.000000 0.010000 (0.011652)
gsub 3.010000 0.000000 3.010000 (3.014059)

和:

1000000
N=10
2.0.0

             用户系统总真实
tr 0.020000 0.000000 0.020000 (0.017230)
gsub 1.900000 0.000000 1.900000 (1.904083)

这是我们可以从更改正则表达式模式中看到的那种差异,这会强制更改获得所需结果所需的处理:

require 'benchmark'

line = '((1,2,3),(1,2,3))'

pattern1 = /\([\d,]+\)/
pattern2 = /\(([\d,]+)\)/
pattern3 = /\((?:\d+,?)+\)/
pattern4 = /\d(?:[\d,])+/

line.scan(pattern1) # => ["(1,2,3)", "(1,2,3)"]
line.scan(pattern2) # => [["1,2,3"], ["1,2,3"]]
line.scan(pattern3) # => ["(1,2,3)", "(1,2,3)"]
line.scan(pattern4) # => ["1,2,3", "1,2,3"]

line.scan(pattern1).map{ |s| s[1..-1].split(',').map(&:to_i) } # => [[1, 2, 3], [1, 2, 3]]
line.scan(pattern2).map{ |s| s[0].split(',').map(&:to_i) }     # => [[1, 2, 3], [1, 2, 3]]
line.scan(pattern3).map{ |s| s[1..-1].split(',').map(&:to_i) } # => [[1, 2, 3], [1, 2, 3]]
line.scan(pattern4).map{ |s| s.split(',').map(&:to_i) }        # => [[1, 2, 3], [1, 2, 3]]

N = 1000000
Benchmark.bm(8) do |x|
  x.report('pattern1') { N.times { line.scan(pattern1).map{ |s| s[1..-1].split(',').map(&:to_i) } }}
  x.report('pattern2') { N.times { line.scan(pattern2).map{ |s| s[0].split(',').map(&:to_i) }     }}
  x.report('pattern3') { N.times { line.scan(pattern3).map{ |s| s[1..-1].split(',').map(&:to_i) } }}
  x.report('pattern4') { N.times { line.scan(pattern4).map{ |s| s.split(',').map(&:to_i) }        }}
end

在 Ruby 2.0-p427 上:

               user     system      total        real
pattern1   5.610000   0.010000   5.620000 (  5.606556)
pattern2   5.460000   0.000000   5.460000 (  5.467228)
pattern3   5.730000   0.000000   5.730000 (  5.731310)
pattern4   5.080000   0.010000   5.090000 (  5.085965)
于 2013-07-25T16:01:58.850 回答
1

目前还不完全清楚您的性能问题在哪里,但就实施而言,有一些事情显然不是最佳的。

如果您正在搜索和替换以删除特定字符,请避免gsub重复运行。处理和重新处理每个字符的相同字符串需要相当长的时间。而是一次性完成:

s.gsub(/[\(\)]/, '')

正则表达式中的[...]符号表示“以下字符的集合”,因此在这种情况下,它是左括号或右括号。

一种更有效的方法是tr用于重新映射或删除单个字符的方法,并且通常更快,因为没有编译或执行正则表达式:

s.tr('()', '')

另一个技巧是,如果您看到的模式包含一个由不带参数的方法调用组成的块:

map { |x| x.to_i }

这折叠成简短的形式:

map(&:to_i)

我不确定基准测试是否更快,但如果确实如此,我不会感到惊讶。那是一个内部生成的过程。

如果您关心绝对速度,您始终可以将性能敏感部分作为Ruby 的 C 或 C++ 扩展。另一种选择是使用JRuby和一些 Java 来完成繁重的工作,如果这样更合适的话,尽管通常 C 会在这样的低级工作中脱颖而出。

于 2013-07-25T14:39:48.077 回答