用Ruby进行XML数据扫描:在性能方面,case语句和动态调度哪个更好?

3

我正在编写一个XML数据扫描器,使用类似nokogiri的XML解析库读取XML文本,并生成节点树。我需要为每个XML元素创建一个对象。因此,我需要创建一个方法,根据给定的元素名称和属性来创建对象,例如:

create_node(name, attributes_hash)

此方法需要根据name进行分支。实现可能的方式有:

  1. 条件语句
  2. 方法分派和预定义方法

由于这种方法可能成为瓶颈,我编写了一个基准测试脚本来检查Ruby的性能。(基准测试脚本附在这个问题的最后部分。我不喜欢脚本的某些部分——特别是如何创建条件语句——所以对于如何改进它的注释也是受欢迎的,但请将其提供为注释而不是答案……我可能还需要为此创建一个问题..)

该脚本测量以下两个范围大小的四种情况:

  1. 使用常量名称进行方法分派
  2. 使用名称与#{}连接进行方法分派
  3. 使用名称与+连接进行方法分派
  4. 使用条件语句调用相同的方法

结果:

                                                 user     system      total        real
a to z: method_calls (with const name)       0.090000   0.000000   0.090000 (  0.092516)
a to z: method_calls (with dynamic name) 1   0.180000   0.000000   0.180000 (  0.181793)
a to z: method_calls (with dynamic name) 2   0.200000   0.000000   0.200000 (  0.202818)
a to z: switch_calls                         0.130000   0.000000   0.130000 (  0.132633)

                                                user     system      total        real
a to zz: method_calls (with const name)       2.900000   0.000000   2.900000 (  2.894273)
a to zz: method_calls (with dynamic name) 1   6.500000   0.010000   6.510000 (  6.507099)
a to zz: method_calls (with dynamic name) 2   6.980000   0.000000   6.980000 (  6.987534)
a to zz: switch_calls                         4.750000   0.000000   4.750000 (  4.742448)

我观察到基于常量名称的方法调度比使用case语句更快,但是,如果在确定方法名称时涉及字符串操作,则确定方法名称的成本要高于实际方法调用成本,从而使选项2和3比选项4慢。此外,选项2和3之间的差异可以忽略不计。

为了使扫描仪安全,我更喜欢在方法前面加上一些前缀,因为如果没有这样做,可能会构造一个XML来调用某些方法,而我不希望发生这种情况。但是确定方法名称的成本并不可忽略。

你如何编写这些扫描器?我想知道以下问题的答案:

  1. 除以上方案之外,是否有其他好的方案?
  2. 如果没有,你会选择哪个方案(case-when或方法调度)?
  3. 如果我不计算方法名称,则更快。有没有一种安全的方法进行方法调度?(例如通过限制要调用的节点名称。)

基准测试脚本

# Benchmark to measure the difference of
# use of case statement and message passing

require 'benchmark'

def bench(title, tobj, count)
  Benchmark.bmbm do |b|
    b.report "#{title}: method_calls (with const name)" do
      (1..count).each do |c|
        tobj.run_send_using_const
      end
    end

    b.report "#{title}: method_calls (with dynamic name) 1" do
      (1..count).each do |c|
        tobj.run_send_using_dynamic_1
      end
    end

    b.report "#{title}: method_calls (with dynamic name) 2" do
      (1..count).each do |c|
        tobj.run_send_using_dynamic_2
      end
    end

    b.report "#{title}: switch_calls" do
      (1..count).each do |c|
        tobj.run_switch
      end
    end
  end
end


class Switcher
  def initialize(names)
    @method_names = { }
    @names = names
    names.each do |n|
      @method_names[n] = "dynamic_#{n}"
      @@n = n
      class << self
        mname = "dynamic_#{@@n}"
        define_method(mname) do
          mname
        end
      end
    end

    swst = ""
    names.each do |n|
      swst << "when \"#{n}\" then dynamic_#{n}\n"
    end

    st = "
    def run_switch_each(n)
      case n
#{swst}
      end
    end
    "
    eval(st)
  end

  def run_send_using_const
    @method_names.each_value do |n|
      self.send n
    end
  end

  def run_send_using_dynamic_1
    @names.each do |n|
      self.send "dynamic_#{n}"
    end
  end

  def run_send_using_dynamic_2
    @names.each do |n|
      self.send "dynamic_" + n
    end
  end

  def run_switch
    @names.each do |n|
      run_switch_each(n)
    end
  end

end


sw1 = Switcher.new('a'..'z')
sw2 = Switcher.new('a'..'zz')

bench("a to z", sw1, 10000)
bench("a to zz", sw2, 10000)

5
只是好奇:为什么你要编写自己的XML解析器?你可以考虑使用现有库,比如Nokogiri。 - Patrick Oscity
2
我同意padde的观点。Ruby有很多高性能的XML解析器库,其中包含快速的C/C++代码或链接到C/C++库。 - Philip
非常感谢您的评论。现在我明白这个问题有误导性了。我并不是想从头开始构建一个解析器。我想编写一个使用XML解析器的数据扫描器,我的问题与那种类型的扫描器有关。我将在今天稍后修改问题以反映这一点。 - shigeya
我有点困惑 - 如果你已经到了对解决方案进行基准测试的阶段,那么你实际上在问什么? - Frederick Cheung
1
你可以将 send 方法设置为私有方法,然后使用 public_send 方法来限制调用公共方法。这样做比直接使用 send 方法会稍微慢一些,但是比使用前缀方法更美观。 - Dmitry
显示剩余3条评论
1个回答

2
我认为这是过早优化的例子。
但是确定方法名称的成本并不可忽略。相对于什么来说成本不可忽略?这里采用了不同的方法,它们有不同的性能指标,但将分派一个节点所需的时间与解析该节点(使用Nokogiri或其他工具),构造专门的节点对象以及执行所需操作的时间相比较,是否相当?
我认为不是。我没有基准来证明这个说法(你需要实际的代码),但字符串连接与字符串插值实际上会在结果中产生明显的差异(dynamic1 vs dynamic2),这是一种很好的指示,说明你正在测量一些微不足道的东西。
或者说,每个分派添加一个字符串连接会使结果时间增加2-2.5倍(const vs dynamic2)。

我觉得我没有很好地提出问题。计算成本中微不足道的部分就像你所说的,是如何确定方法名称的差异。但是你说,“这是明显的差异,但测量一些微不足道的东西?”如果你的意思是两种动态情况之间的差异可以忽略不计,那我同意。但关键是——由于我提问的方式不对——是否有其他可能的方案来高效地完成此操作,例如switch、方法调度(使用动态名称)或其他方案。(无论如何,非常感谢您) - shigeya
让我重新表述。1)字符串拼接和字符串插值之间的性能差异只在微基准测试中才可见。但是,请看,这里我们看到它在数字(0.02秒和0.5秒)上产生了明显的差异。2)最高和最低执行选项之间唯一的区别是每个调度中有一个字符串连接,使用短字符串。实际上,这会使基准测试时间增加约2.2倍。这基本上保证了您在此处测量的所有内容所花费的时间非常少,并且一旦添加业务逻辑,它将不会产生影响。 - Dmitry
不,这并不可忽略。我需要找到一个更好的解决方案,即使只有微小的差异。我没有说明该方法可能被调用多少次。如果它只被调用<10K次(仅作为示例),那么这可能不是问题。我在这里提出这个问题并设置赏金的原因是,我希望尽可能地降低成本,因为我处理大量节点的大型XML文件需要使用该方法,并且我需要以交互方式处理文件(我正在编辑大型文件并进行处理,等待和查看,例如每5分钟),因此速度不可忽略。 - shigeya

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