From 8ef75024b96d3e5279b39fdd1692821cdbcd84b5 Mon Sep 17 00:00:00 2001
From: NAITOH Jun <naitoh@gmail.com>
Date: Sun, 27 Oct 2024 15:02:18 +0900
Subject: [PATCH] Add `IOSource#match?` method (#216)
## Why?
`StringScanner#match?` is faster than `StringScanner#check`.
See: https://github.com/ruby/strscan/pull/111
## Benchmark
```
RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.4/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml
ruby 3.3.4 (2024-07-09 revision be1089c8ec) [arm64-darwin22]
Calculating -------------------------------------
before after before(YJIT) after(YJIT)
dom 18.819 19.362 32.846 34.708 i/s - 100.000 times in 5.313905s 5.164791s 3.044500s 2.881200s
sax 28.188 29.982 48.386 52.554 i/s - 100.000 times in 3.547597s 3.335304s 2.066732s 1.902809s
pull 31.962 33.902 57.868 60.662 i/s - 100.000 times in 3.128689s 2.949690s 1.728071s 1.648467s
stream 31.436 33.030 52.808 56.647 i/s - 100.000 times in 3.181095s 3.027574s 1.893635s 1.765304s
Comparison:
dom
after(YJIT): 34.7 i/s
before(YJIT): 32.8 i/s - 1.06x slower
after: 19.4 i/s - 1.79x slower
before: 18.8 i/s - 1.84x slower
sax
after(YJIT): 52.6 i/s
before(YJIT): 48.4 i/s - 1.09x slower
after: 30.0 i/s - 1.75x slower
before: 28.2 i/s - 1.86x slower
pull
after(YJIT): 60.7 i/s
before(YJIT): 57.9 i/s - 1.05x slower
after: 33.9 i/s - 1.79x slower
before: 32.0 i/s - 1.90x slower
stream
after(YJIT): 56.6 i/s
before(YJIT): 52.8 i/s - 1.07x slower
after: 33.0 i/s - 1.72x slower
before: 31.4 i/s - 1.80x slower
```
- YJIT=ON : 1.05x - 1.09x faster
- YJIT=OFF : 1.02x - 1.06x faster
Co-authored-by: Sutou Kouhei <kou@clear-code.com>
.bundle/gems/rexml-3.3.9/lib/rexml/parsers/baseparser.rb | 84 ++++++++++++++++-----------------
.bundle/gems/rexml-3.3.9/lib/rexml/source.rb | 35 ++++++++++++++
2 files changed, 77 insertions(+), 42 deletions(-)
@@ -269,10 +269,10 @@ module REXML
@source.ensure_buffer
if @document_status == nil
start_position = @source.position
- if @source.match("<?", true)
+ if @source.match?("<?", true)
return process_instruction
- elsif @source.match("<!", true)
- if @source.match("--", true)
+ elsif @source.match?("<!", true)
+ if @source.match?("--", true)
md = @source.match(/(.*?)-->/um, true)
if md.nil?
raise REXML::ParseException.new("Unclosed comment", @source)
@@ -281,10 +281,10 @@ module REXML
raise REXML::ParseException.new("Malformed comment", @source)
end
return [ :comment, md[1] ]
- elsif @source.match("DOCTYPE", true)
+ elsif @source.match?("DOCTYPE", true)
base_error_message = "Malformed DOCTYPE"
- unless @source.match(/\s+/um, true)
- if @source.match(">")
+ unless @source.match?(/\s+/um, true)
+ if @source.match?(">")
message = "#{base_error_message}: name is missing"
else
message = "#{base_error_message}: invalid name"
@@ -293,10 +293,10 @@ module REXML
raise REXML::ParseException.new(message, @source)
end
name = parse_name(base_error_message)
- if @source.match(/\s*\[/um, true)
+ if @source.match?(/\s*\[/um, true)
id = [nil, nil, nil]
@document_status = :in_doctype
- elsif @source.match(/\s*>/um, true)
+ elsif @source.match?(/\s*>/um, true)
id = [nil, nil, nil]
@document_status = :after_doctype
@source.ensure_buffer
@@ -308,9 +308,9 @@ module REXML
# For backward compatibility
id[1], id[2] = id[2], nil
end
- if @source.match(/\s*\[/um, true)
+ if @source.match?(/\s*\[/um, true)
@document_status = :in_doctype
- elsif @source.match(/\s*>/um, true)
+ elsif @source.match?(/\s*>/um, true)
@document_status = :after_doctype
@source.ensure_buffer
else
@@ -320,7 +320,7 @@ module REXML
end
args = [:start_doctype, name, *id]
if @document_status == :after_doctype
- @source.match(/\s*/um, true)
+ @source.match?(/\s*/um, true)
@stack << [ :end_doctype ]
end
return args
@@ -331,14 +331,14 @@ module REXML
end
end
if @document_status == :in_doctype
- @source.match(/\s*/um, true) # skip spaces
+ @source.match?(/\s*/um, true) # skip spaces
start_position = @source.position
- if @source.match("<!", true)
- if @source.match("ELEMENT", true)
+ if @source.match?("<!", true)
+ if @source.match?("ELEMENT", true)
md = @source.match(/(.*?)>/um, true)
raise REXML::ParseException.new( "Bad ELEMENT declaration!", @source ) if md.nil?
return [ :elementdecl, "<!ELEMENT" + md[1] ]
- elsif @source.match("ENTITY", true)
+ elsif @source.match?("ENTITY", true)
match_data = @source.match(Private::ENTITYDECL_PATTERN, true)
unless match_data
raise REXML::ParseException.new("Malformed entity declaration", @source)
@@ -370,7 +370,7 @@ module REXML
end
match << '%' if ref
return match
- elsif @source.match("ATTLIST", true)
+ elsif @source.match?("ATTLIST", true)
md = @source.match(Private::ATTLISTDECL_END, true)
raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil?
element = md[1]
@@ -390,10 +390,10 @@ module REXML
end
end
return [ :attlistdecl, element, pairs, contents ]
- elsif @source.match("NOTATION", true)
+ elsif @source.match?("NOTATION", true)
base_error_message = "Malformed notation declaration"
- unless @source.match(/\s+/um, true)
- if @source.match(">")
+ unless @source.match?(/\s+/um, true)
+ if @source.match?(">")
message = "#{base_error_message}: name is missing"
else
message = "#{base_error_message}: invalid name"
@@ -405,7 +405,7 @@ module REXML
id = parse_id(base_error_message,
accept_external_id: true,
accept_public_id: true)
- unless @source.match(/\s*>/um, true)
+ unless @source.match?(/\s*>/um, true)
message = "#{base_error_message}: garbage before end >"
raise REXML::ParseException.new(message, @source)
end
@@ -419,7 +419,7 @@ module REXML
end
elsif match = @source.match(/(%.*?;)\s*/um, true)
return [ :externalentity, match[1] ]
- elsif @source.match(/\]\s*>/um, true)
+ elsif @source.match?(/\]\s*>/um, true)
@document_status = :after_doctype
return [ :end_doctype ]
end
@@ -428,16 +428,16 @@ module REXML
end
end
if @document_status == :after_doctype
- @source.match(/\s*/um, true)
+ @source.match?(/\s*/um, true)
end
begin
start_position = @source.position
- if @source.match("<", true)
+ if @source.match?("<", true)
# :text's read_until may remain only "<" in buffer. In the
# case, buffer is empty here. So we need to fill buffer
# here explicitly.
@source.ensure_buffer
- if @source.match("/", true)
+ if @source.match?("/", true)
@namespaces_restore_stack.pop
last_tag = @tags.pop
md = @source.match(Private::CLOSE_PATTERN, true)
@@ -452,7 +452,7 @@ module REXML
raise REXML::ParseException.new(message, @source)
end
return [ :end_element, last_tag ]
- elsif @source.match("!", true)
+ elsif @source.match?("!", true)
md = @source.match(/([^>]*>)/um)
#STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}"
raise REXML::ParseException.new("Malformed node", @source) unless md
@@ -470,7 +470,7 @@ module REXML
end
raise REXML::ParseException.new( "Declarations can only occur "+
"in the doctype declaration.", @source)
- elsif @source.match("?", true)
+ elsif @source.match?("?", true)
return process_instruction
else
# Get the next tag
@@ -651,7 +651,7 @@ module REXML
def parse_name(base_error_message)
md = @source.match(Private::NAME_PATTERN, true)
unless md
- if @source.match(/\S/um)
+ if @source.match?(/\S/um)
message = "#{base_error_message}: invalid name"
else
message = "#{base_error_message}: name is missing"
@@ -693,34 +693,34 @@ module REXML
accept_public_id:)
public = /\A\s*PUBLIC/um
system = /\A\s*SYSTEM/um
- if (accept_external_id or accept_public_id) and @source.match(/#{public}/um)
- if @source.match(/#{public}(?:\s+[^'"]|\s*[\[>])/um)
+ if (accept_external_id or accept_public_id) and @source.match?(/#{public}/um)
+ if @source.match?(/#{public}(?:\s+[^'"]|\s*[\[>])/um)
return "public ID literal is missing"
end
- unless @source.match(/#{public}\s+#{PUBIDLITERAL}/um)
+ unless @source.match?(/#{public}\s+#{PUBIDLITERAL}/um)
return "invalid public ID literal"
end
if accept_public_id
- if @source.match(/#{public}\s+#{PUBIDLITERAL}\s+[^'"]/um)
+ if @source.match?(/#{public}\s+#{PUBIDLITERAL}\s+[^'"]/um)
return "system ID literal is missing"
end
- unless @source.match(/#{public}\s+#{PUBIDLITERAL}\s+#{SYSTEMLITERAL}/um)
+ unless @source.match?(/#{public}\s+#{PUBIDLITERAL}\s+#{SYSTEMLITERAL}/um)
return "invalid system literal"
end
"garbage after system literal"
else
"garbage after public ID literal"
end
- elsif accept_external_id and @source.match(/#{system}/um)
- if @source.match(/#{system}(?:\s+[^'"]|\s*[\[>])/um)
+ elsif accept_external_id and @source.match?(/#{system}/um)
+ if @source.match?(/#{system}(?:\s+[^'"]|\s*[\[>])/um)
return "system literal is missing"
end
- unless @source.match(/#{system}\s+#{SYSTEMLITERAL}/um)
+ unless @source.match?(/#{system}\s+#{SYSTEMLITERAL}/um)
return "invalid system literal"
end
"garbage after system literal"
else
- unless @source.match(/\A\s*(?:PUBLIC|SYSTEM)\s/um)
+ unless @source.match?(/\A\s*(?:PUBLIC|SYSTEM)\s/um)
return "invalid ID type"
end
"ID type is missing"
@@ -729,7 +729,7 @@ module REXML
def process_instruction
name = parse_name("Malformed XML: Invalid processing instruction node")
- if @source.match(/\s+/um, true)
+ if @source.match?(/\s+/um, true)
match_data = @source.match(/(.*?)\?>/um, true)
unless match_data
raise ParseException.new("Malformed XML: Unclosed processing instruction", @source)
@@ -737,7 +737,7 @@ module REXML
content = match_data[1]
else
content = nil
- unless @source.match("?>", true)
+ unless @source.match?("?>", true)
raise ParseException.new("Malformed XML: Unclosed processing instruction", @source)
end
end
@@ -767,9 +767,9 @@ module REXML
expanded_names = {}
closed = false
while true
- if @source.match(">", true)
+ if @source.match?(">", true)
return attributes, closed
- elsif @source.match("/>", true)
+ elsif @source.match?("/>", true)
closed = true
return attributes, closed
elsif match = @source.match(QNAME, true)
@@ -777,7 +777,7 @@ module REXML
prefix = match[2]
local_part = match[3]
- unless @source.match(/\s*=\s*/um, true)
+ unless @source.match?(/\s*=\s*/um, true)
message = "Missing attribute equal: <#{name}>"
raise REXML::ParseException.new(message, @source)
end
@@ -793,7 +793,7 @@ module REXML
message = "Missing attribute value end quote: <#{name}>: <#{quote}>"
raise REXML::ParseException.new(message, @source)
end
- @source.match(/\s*/um, true)
+ @source.match?(/\s*/um, true)
if prefix == "xmlns"
if local_part == "xml"
if value != Private::XML_PREFIXED_NAMESPACE
@@ -18,6 +18,16 @@ module REXML
pattern = /#{Regexp.escape(pattern)}/ if pattern.is_a?(String)
super(pattern)
end
+
+ def match?(pattern)
+ pattern = /#{Regexp.escape(pattern)}/ if pattern.is_a?(String)
+ super(pattern)
+ end
+
+ def skip(pattern)
+ pattern = /#{Regexp.escape(pattern)}/ if pattern.is_a?(String)
+ super(pattern)
+ end
end
end
using StringScannerCheckScanString
@@ -126,6 +136,14 @@ module REXML
end
end
+ def match?(pattern, cons=false)
+ if cons
+ !@scanner.skip(pattern).nil?
+ else
+ !@scanner.match?(pattern).nil?
+ end
+ end
+
def position
@scanner.pos
end
@@ -267,6 +285,23 @@ module REXML
md.nil? ? nil : @scanner
end
+ def match?( pattern, cons=false )
+ # To avoid performance issue, we need to increase bytes to read per scan
+ min_bytes = 1
+ while true
+ if cons
+ n_matched_bytes = @scanner.skip(pattern)
+ else
+ n_matched_bytes = @scanner.match?(pattern)
+ end
+ return true if n_matched_bytes
+ return false if pattern.is_a?(String)
+ return false if @source.nil?
+ return false unless read(nil, min_bytes)
+ min_bytes *= 2
+ end
+ end
+
def empty?
super and ( @source.nil? || @source.eof? )
end
--
2.27.0