我正在编写一个XML数据扫描器,使用类似nokogiri的XML解析库读取XML文本,并生成节点树。我需要为每个XML元素创建一个对象。因此,我需要创建一个方法,根据给定的元素名称和属性来创建对象,例如:
create_node(name, attributes_hash)
此方法需要根据name
进行分支。实现可能的方式有:
- 条件语句
- 方法分派和预定义方法
由于这种方法可能成为瓶颈,我编写了一个基准测试脚本来检查Ruby的性能。(基准测试脚本附在这个问题的最后部分。我不喜欢脚本的某些部分——特别是如何创建条件语句——所以对于如何改进它的注释也是受欢迎的,但请将其提供为注释而不是答案……我可能还需要为此创建一个问题..)
该脚本测量以下两个范围大小的四种情况:
- 使用常量名称进行方法分派
- 使用名称与
#{}
连接进行方法分派 - 使用名称与
+
连接进行方法分派 - 使用条件语句调用相同的方法
结果:
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来调用某些方法,而我不希望发生这种情况。但是确定方法名称的成本并不可忽略。
你如何编写这些扫描器?我想知道以下问题的答案:
- 除以上方案之外,是否有其他好的方案?
- 如果没有,你会选择哪个方案(case-when或方法调度)?
- 如果我不计算方法名称,则更快。有没有一种安全的方法进行方法调度?(例如通过限制要调用的节点名称。)
基准测试脚本
# 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)
send
方法设置为私有方法,然后使用public_send
方法来限制调用公共方法。这样做比直接使用send
方法会稍微慢一些,但是比使用前缀方法更美观。 - Dmitry