Skip to content

Commit 3b026f8

Browse files
makenowjustkou
andauthored
Improve Element#attribute implementation as 6500x faster (#146)
`Element#namespaces` is heavy method because this method needs to traverse all ancestors of the element. `Element#attribute` calls `namespaces` redundantly, so it is much slower. This PR reduces `namespaces` calls in `Element#attribute`. Also, this PR removes a redundant `respond_to?` because `namespaces` must return `Hash` in the current implementation. Below is the result of a benchmark for this on my laptop. ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/makenowjust/Projects/github.com/makenowjust/simple-dotfiles/.asdf/installs/ruby/3.3.2/bin/ruby -v -S benchmark-driver /Users/makenowjust/Projects/github.com/ruby/rexml/benchmark/attribute.yaml ruby 3.3.2 (2024-05-30 revision e5a195edf6) [arm64-darwin23] Calculating ------------------------------------- rexml 3.2.6 master 3.2.6(YJIT) master(YJIT) attribute_with_ns 425.420 849.271 5.336k 10.629k i/s - 1.000k times in 2.350620s 1.177481s 0.187416s 0.094084s attribute_without_ns 834.750 5.587M 10.656k 2.950M i/s - 1.000k times in 1.197963s 0.000179s 0.093846s 0.000339s Comparison: attribute_with_ns master(YJIT): 10628.8 i/s 3.2.6(YJIT): 5335.7 i/s - 1.99x slower master: 849.3 i/s - 12.52x slower rexml 3.2.6: 425.4 i/s - 24.98x slower attribute_without_ns master: 5586593.2 i/s master(YJIT): 2949854.4 i/s - 1.89x slower 3.2.6(YJIT): 10655.8 i/s - 524.28x slower rexml 3.2.6: 834.8 i/s - 6692.53x slower ``` This result shows that `Element#attribute` is now 6500x faster than the old implementation if `namespace` is not supplied. It seems strange that it is slower when YJIT is enabled, but we believe this is a separate issue. Thank you. --------- Co-authored-by: Sutou Kouhei <[email protected]>
1 parent b5bf109 commit 3b026f8

File tree

2 files changed

+40
-7
lines changed

2 files changed

+40
-7
lines changed

benchmark/attribute.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
loop_count: 1000
2+
contexts:
3+
- gems:
4+
rexml: 3.2.6
5+
require: false
6+
prelude: require 'rexml'
7+
- name: master
8+
prelude: |
9+
$LOAD_PATH.unshift(File.expand_path("lib"))
10+
require 'rexml'
11+
- name: 3.2.6(YJIT)
12+
gems:
13+
rexml: 3.2.6
14+
require: false
15+
prelude: |
16+
require 'rexml'
17+
RubyVM::YJIT.enable
18+
- name: master(YJIT)
19+
prelude: |
20+
$LOAD_PATH.unshift(File.expand_path("lib"))
21+
require 'rexml'
22+
RubyVM::YJIT.enable
23+
24+
prelude: |
25+
require 'rexml/document'
26+
27+
xml_source = "<deepest x:with_ns='foo' without_ns='bar'></deepest>"
28+
100.times do
29+
xml_source = "<nest>#{xml_source}</nest>"
30+
end
31+
xml_source = "<root xmlns:x='xyz'>#{xml_source}</root>"
32+
33+
document = REXML::Document.new(xml_source)
34+
deepest_node = document.elements["//deepest"]
35+
36+
benchmark:
37+
with_ns: deepest_node.attribute("with_ns", "xyz")
38+
without_ns: deepest_node.attribute("without_ns")

lib/rexml/element.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,16 +1276,11 @@ def [](name_or_index)
12761276
# document.root.attribute("x", "a") # => a:x='a:x'
12771277
#
12781278
def attribute( name, namespace=nil )
1279-
prefix = nil
1280-
if namespaces.respond_to? :key
1281-
prefix = namespaces.key(namespace) if namespace
1282-
else
1283-
prefix = namespaces.index(namespace) if namespace
1284-
end
1279+
prefix = namespaces.key(namespace) if namespace
12851280
prefix = nil if prefix == 'xmlns'
12861281

12871282
ret_val =
1288-
attributes.get_attribute( "#{prefix ? prefix + ':' : ''}#{name}" )
1283+
attributes.get_attribute( prefix ? "#{prefix}:#{name}" : name )
12891284

12901285
return ret_val unless ret_val.nil?
12911286
return nil if prefix.nil?

0 commit comments

Comments
 (0)