【问题标题】:Why is String#split("\n") and Array#join(' ') quicker than String#gsub(/\n/, ' ')?为什么 String#split("\n") 和 Array#join(' ') 比 String#gsub(/\n/, ' ') 快?
【发布时间】:2015-04-29 23:38:14
【问题描述】:

我必须从大量字符串中删除所有换行符。在对 string.join("\n").split(' ')string.gsub(/\n/, ' ') 进行基准测试时,我发现 split 和 join 方法要快得多,但很难理解为什么。我不明白每次遇到\n 时如何将字符串拆分为数组元素,然后将数组加入新字符串可能比扫描并用' ' 替换每个\n 更快。

sentence = %q[
  Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
  totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
  dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
  sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam
  est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius
  modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima
  veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea]

检查以验证两种方法的输出确实相同:

puts sentence.split("\n").join(' ') == sentence.gsub(/\n/, ' ')
#=> true

用于基准测试的脚本:

def split_join_method(string)
  start = Time.now;
  1000000.times {  string.split("\n").join(' ') }
  puts "split_join: #{Time.now - start} s"
end

def gsub_method(string)
  start = Time.now;
  1000000.times {  string.gsub(/\n/, ' ') }
  puts "gsub: #{Time.now - start} s"
end

5.times do
  split_join_method(sentence)
  gsub_method(sentence)
end

结果:

#=> split_join: 6.753057 s
#=> gsub: 14.938358 s
#=> split_join: 6.16101 s
#=> gsub: 14.166971 s
#=> split_join: 5.946168 s
#=> gsub: 13.490355 s
#=> split_join: 5.781062 s
#=> gsub: 13.436135 s
#=> split_join: 5.903052 s
#=> gsub: 15.670774 s

【问题讨论】:

  • 我不知道为什么 gsub 这么慢,但你会发现 string.tr("\n", " ") 比你当前的任何一种方法都快。
  • 你也应该尝试一个更大的字符串,否则正则表达式编译很可能会支配 gsub 的运行时间

标签: ruby regex string


【解决方案1】:

我认为gsub 需要更多时间有两个原因:

首先是使用正则表达式引擎有一个初始成本,至少要解析模式。

第二个可能也是最重要的一点是,当拆分(此处为文字字符串)使用快速字符串搜索算法时,正则表达式引擎逐个字符地使用哑步行并测试字符串中每个位置的模式(可能是 Boyer-Moore 算法)。

请注意,即使拆分/连接方式更快,它也可能使用更多内存,因为这种方式需要生成新的字符串。

注意 2:一些正则表达式引擎能够在步行前使用这种快速字符串搜索算法来查找位置,但我没有关于 ruby​​ 正则表达式引擎的信息。

注意3:更好地了解包含重复次数少但字符串较大的测试会发生什么可能会很有趣。 [编辑] 在使用@spickermann 代码进行多次测试后,即使重复次数很少,它似乎也没有改变任何东西(或没有什么非常有意义的东西)。所以初始成本可能并不那么重要。

【讨论】:

  • 第二个参数有些无效。首先是因为我认为大多数现代正则表达式引擎都不是这样,其次因为模式的长度为 1
  • @NiklasB.:ruby 正则表达式引擎确实是一个相对先进的正则表达式引擎(有很多功能),但正如我所说,我没有关于预分析的信息(和确认)。如果您有关于此的链接,请不要犹豫分享。但是我认为模式长度并不重要,唯一重要的是模式是否是文字字符串(或包含/以文字字符串开头)。
  • @NiklasB.:其他问题:正则表达式引擎如何考虑\n?作为文字换行符还是作为表示模式中的换行符的特殊字符序列?
  • /\n/ 是一个简单的换行符,就像"\n" 一样,\n 在正则表达式引擎看到任何内容之前转换为换行符。
  • @muistooshort:我不认为是这样,例如,如果您在控制台中输入"\p{L}",它会打印"p{L}"(并不奇怪)。这个简单的测试可能证明正则表达式引擎没有使用内置字符串解释器来处理模式。
【解决方案2】:

您的问题是比较苹果和橙子,因为您将正则表达式方法与字符串搜索操作进行比较。

我的基准测试无法重现您的观察结果,即split 结合join 通常比简单的gsub 更快,gsub 版本总是更快。我唯一可以确认的是,正则表达式匹配比字符串搜索慢,这并不奇怪。

顺便说一句。 tr是此类问题最快的解决方案:

Rehearsal ---------------------------------------------------
string_split:     5.390000   0.100000   5.490000 (  5.480459)
regexp_split:    14.220000   0.160000  14.380000 ( 14.413509)
string_gsub :     3.750000   0.090000   3.840000 (  3.832316)
regexp_gsub :    12.890000   0.130000  13.020000 ( 13.045899)
string_tr   :     2.480000   0.050000   2.530000 (  2.525891)
----------------------------------------- total: 39.260000sec

                      user     system      total        real
string_split:     5.450000   0.090000   5.540000 (  5.543735)
regexp_split:    14.340000   0.190000  14.530000 ( 14.552214)
string_gsub :     4.160000   0.120000   4.280000 (  4.543941)
regexp_gsub :    13.570000   0.200000  13.770000 ( 14.356955)
string_tr   :     2.390000   0.040000   2.430000 (  2.431676)

我用于此基准测试的代码:

require 'benchmark'

@string = %q[
  Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
  totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
  dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
  sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam
  est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius
  modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima
  veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea]

def string_split
  @string.split("\n").join(' ')
end

def regexp_split
  @string.split(/\n/).join(' ')
end

def string_gsub
  @string.gsub("\n", ' ')
end

def regexp_gsub
  @string.gsub(/\n/, ' ')
end

def string_tr
  @string.tr("\n", ' ')
end

n = 1_000_000
Benchmark.bmbm(15) do |x|
  x.report("string_split:")   { n.times do; string_split; end }
  x.report("regexp_split:")   { n.times do; regexp_split; end }
  x.report("string_gsub :")   { n.times do; string_gsub ; end }
  x.report("regexp_gsub :")   { n.times do; regexp_gsub ; end }
  x.report("string_tr   :")   { n.times do; string_tr   ; end }
end

【讨论】:

  • 有趣,我已经测试了这段代码,但我得到了完全不同的结果。简而言之,regexp_splitstring_gsubregexp_gsub (~11s) 之间没有区别。其他测试也给出了类似的结果。
【解决方案3】:

这是因为在您的 gsub 代码中,您使用的是正则表达式,由于 Casimir 的回答中指出的原因,这很慢。这是证据:如果你改变了

string.gsub(/\n/, ' ')

string.gsub("\n", ' ')

那么 gsub 代码实际上比 split/join 代码

【讨论】:

    猜你喜欢
    • 2023-03-31
    • 2016-12-31
    • 1970-01-01
    • 2012-06-09
    • 2016-04-15
    • 1970-01-01
    • 2016-11-09
    • 1970-01-01
    相关资源
    最近更新 更多