使用Ruby的XML数据扫描仪:case语句还是性能方面的动态调度?
我正在编写一个XML数据扫描仪,它使用一些XML解析器库(比如nokogiri)读取XML文本, 并生成一个节点树。我需要为每个XML元素创建一个对象。 所以,我需要一个方法,根据给定的元素名和属性创建一个对象,如下所示, 无论我使用哪种解析器库选项(SAX或DOM):使用Ruby的XML数据扫描仪:case语句还是性能方面的动态调度?,ruby,performance,xml-parsing,Ruby,Performance,Xml Parsing,我正在编写一个XML数据扫描仪,它使用一些XML解析器库(比如nokogiri)读取XML文本, 并生成一个节点树。我需要为每个XML元素创建一个对象。 所以,我需要一个方法,根据给定的元素名和属性创建一个对象,如下所示, 无论我使用哪种解析器库选项(SAX或DOM): create_node(name, attributes_hash) 此方法需要根据名称进行分支。实施的可能性是: 案例陈述 方法分派和预定义方法 由于这种方法可能成为瓶颈,我编写了一个基准脚本来检查Ruby的性能。(这个问题
create_node(name, attributes_hash)
此方法需要根据名称进行分支。实施的可能性是:
案例陈述
方法分派和预定义方法
由于这种方法可能成为瓶颈,我编写了一个基准脚本来检查Ruby的性能。(这个问题的最后一部分附带了基准脚本。我不喜欢脚本的某些部分——特别是如何创建案例陈述——所以我也欢迎对如何改进这一点发表评论,但请将其作为评论而不是答案提供……我可能也需要为此创建一个问题……)
脚本以两种范围大小度量以下四种情况:
具有常量名称的方法分派
名为concatenate with#{}
名称为的方法分派与+
使用case语句调用相同的方法
结果:
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)
我观察到基于const name的方法分派比使用case语句快,但是,如果在确定方法名称时涉及字符串操作,则确定方法名称的成本比实际的方法调用成本更高,实际上使这些选项(2和3)比选项4慢。此外,方案2和方案3之间的差异可以忽略不计
为了使扫描器更安全,我更喜欢在方法前面加上前缀,因为如果没有前缀,就可以创建一个XML来调用某些方法,这是我不希望发生的。但是,确定方法名称的成本是不可忽略的
这些扫描仪是怎么写的?我想知道以下问题的答案:
除了上面提到的,还有什么好的方案吗
如果没有,您选择哪种(情况或方法)方案
如果我不计算方法名,它会更快。有没有安全地执行方法分派的好方法?(例如,通过限制要调用的节点名。)
基准脚本
#用于测量
#case语句和消息传递的使用
需要“基准”
def台架(标题、tobj、计数)
Benchmark.bm do|b|
b、 报告“#{title}:方法_调用(使用const name)”do
(1.计数)。每个都做|
tobj.run\u send\u使用常数
结束
结束
b、 报告“#{title}:方法_调用(使用动态名称)1”do
(1.计数)。每个都做|
tobj.run_send_使用_dynamic_1
结束
结束
b、 报告“#{title}:方法_调用(使用动态名称)2”do
(1.计数)。每个都做|
tobj.run_send_使用_dynamic_2
结束
结束
b、 报告“#{title}:switch_calls”do
(1.计数)。每个都做|
tobj.run_开关
结束
结束
结束
结束
类切换器
def初始化(名称)
@方法_name={}
@名称=名称
每个人的名字|
@方法_名称[n]=“动态_35;{n}”
@@n=n
类我认为这是一种过早优化的情况
但是,确定方法名称的成本是不可忽略的
与什么相比不可忽略?这里的方法有不同的性能数字,但是调度一个节点所花费的时间是否与解析节点(使用Nokogiri或等等)、构造专门的节点对象以及使用它执行任何需要的操作所花费的时间相当
我相信不会的。我没有一个基准来证明这个说法(你需要实际的代码来证明),但事实上,字符串串联与字符串插值实际上在结果上产生了显著的差异(dynamic1与dynamic2),这是一个很好的指标,表明你在衡量一些琐碎的东西
或者每个分派添加一个字符串连接会将结果时间增加2-2.5倍(const vs dynamic2)。只是好奇:为什么要编写自己的XML解析器?你可以看看像Nokogiri这样的现有图书馆。我同意padde的观点。有相当多的Ruby高性能XML解析器库包含快速C/C++代码或与C/C++-libs的链接。非常感谢您的评论。现在我明白这个问题有误导性。我不是有意从头开始创造一个过路人。我想用XML解析器编写一个数据扫描器,我的问题是关于这种扫描器。今天晚些时候,我将修改这个问题以反映这一点。我很困惑——如果你是一个专家,你正在为你的问题制定基准解决方案,你到底在问什么?你可以在分派类上设置send
私有,并使用public\u send
而不是send
将分派的调用仅限于公共方法。这比简单的send
要慢一点,但在美学上比使用带前缀的方法要好。我想我问的问题不太好。正如您所说,计算成本中可忽略的部分是如何确定方法名称之间的差异。但是你说,“这是明显的区别,但是测量一些琐碎的东西?”如果你的意思是,两种动态情况之间的差异可以忽略不计,我同意。但问题是——因为我问问题的方式是错误的——切换、方法调度(使用动态名称)还是其他可能的方案可以有效地实现这一点。(无论如何,非常感谢)让我重新措辞。1) 字符串串联和字符串插值之间的性能差异仅在微基准中可见。但是看,我们看到它在数字上有明显的差别(0.02秒和0.5秒)。2) 最高性能选项和最低性能选项之间的唯一区别是每个分派1(!)字符串串联,带有短字符串。它实际上增加了基准测试时间~2.2倍。那个巴西卡
# 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)