Skip to content

JRuby performance regression for CSV.parse_line #361

Description

@donoghuc

When run with JRuby, CSV.parse_line (and any CSV#each iteration) has a noticeable performance regression because CSV#each pulls rows one at a time through Enumerator#next.

CSV.parse_line is defined as new(line, **options).each.first

csv/lib/csv.rb

Lines 1898 to 1899 in b36c160

defparse_line(line, **options)
new(line, **options).each.first

CSV#each iterates by repeatedly calling parser_enumerator.next

csv/lib/csv.rb

Lines 2689 to 2697 in b36c160

defeach(&block)
returnto_enum(__method__)unlessblock_given?
begin
whiletrue
yield(parser_enumerator.next)
end
rescueStopIteration
end
end

defeach(&block)returnto_enum(__method__)unlessblock_given?beginwhiletrueyield(parser_enumerator.next)endrescueStopIterationendend

parser_enumerator is parser.parse (an Enumerator

csv/lib/csv.rb

Lines 2974 to 2975 in b36c160

defparser_enumerator
@parser_enumerator ||= parser.parse

Per the Ruby documentation, external iteration with next "differs significantly from internal iteration due to using a Fiber" and "the Fiber adds some overhead compared to internal enumeration" https://docs.ruby-lang.org/en/3.4/Enumerator.html

Change that introduced it

CSV#each previously called parser_enumerator.each(&block) (internal iteration, no Fiber). It was changed to the Enumerator#next loop in commit acc05116c55579b78925dd27024969b9daf69135 ("All Enumerable based methods consume the same lines with other methods", acc0511), to fix GH-260 (#260). That commit message itself notes: "This may have a performance penalty. We should benchmark this." It was first released in v3.2.6

Reproducer

require"csv"require"benchmark"ROW=(1..30).map{ |i| "field_#{i}"}.join(",")N=50_000defrate(n,s)=(n / s).roundN.times{CSV.parse_line(ROW)}parse_line=rate(N,Benchmark.realtime{N.times{CSV.parse_line(ROW)}})en=rate(N,Benchmark.realtime{N.times{Enumerator.new{ |y| y << 1}.next}})eb=rate(N,Benchmark.realtime{N.times{Enumerator.new{ |y| y << 1}.each{ |v| breakv}}})engine=defined?(JRUBY_VERSION) ? "JRuby #{JRUBY_VERSION}" : RUBY_ENGINEputs"#{engine} Ruby #{RUBY_VERSION} csv #{CSV::VERSION}"puts"CSV.parse_line : #{parse_line} calls/sec"puts"bare Enumerator#next : #{en} calls/sec"puts"bare Enumerator#each+break : #{eb} calls/sec"

Output, JRuby 10.0.5.0 (via jruby-complete-10.0.5.0.jar, csv 3.3.5):

JRuby 10.0.5.0 Ruby 3.4.5 csv 3.3.5
CSV.parse_line : 12270 calls/sec
bare Enumerator#next : 27411 calls/sec
bare Enumerator#each+break : 447421 calls/sec

Output, MRI CRuby 3.4.10 (csv 3.3.5), for contrast:

ruby Ruby 3.4.10 csv 3.3.5
CSV.parse_line : 46946 calls/sec
bare Enumerator#next : 291157 calls/sec
bare Enumerator#each+break : 1159496 calls/sec

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions