ruby-concurrency / concurrent-ruby

Modern concurrency tools including agents, futures, promises, thread pools, supervisors, and more. Inspired by Erlang, Clojure, Scala, Go, Java, JavaScript, and classic concurrency patterns.
https://ruby-concurrency.github.io/concurrent-ruby/
Other
5.68k stars 418 forks source link

Optimize Concurrent::Map#[] on CRuby by letting the backing Hash handle the default_proc #989

Closed eregon closed 1 year ago

eregon commented 1 year ago
* On ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
  Before:
  Hash#[]     14.477M (± 2.7%) i/s -     72.882M in   5.038078s
   Map#[]      7.837M (± 1.7%) i/s -     39.947M in   5.098411s
  After:
  Hash#[]     14.340M (± 1.5%) i/s -     72.074M in   5.027414s
   Map#[]      9.840M (± 0.8%) i/s -     50.106M in   5.092390s

With this, Concurrent::Map#[] on CRuby is just:

      def [](key)
        @backend[key] # @backend is the Hash
      end

i.e., just one extra call and ivar read compared to Hash#[].

eregon commented 1 year ago

I also measured on ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-linux] and with YJIT.

Before: (without the last commit of this PR)
             Hash#[]     16.102M (± 1.9%) i/s -     81.399M in   5.057130s
              Map#[]      8.296M (± 1.0%) i/s -     41.992M in   5.061993s
With YJIT:
             Hash#[]     17.601M (± 0.0%) i/s -     88.445M in   5.025111s
              Map#[]     17.058M (± 0.3%) i/s -     85.718M in   5.025048s

After:
             Hash#[]     15.736M (± 4.0%) i/s -     79.505M in   5.061376s
              Map#[]     11.846M (± 1.4%) i/s -     59.857M in   5.053804s
With YJIT:
             Hash#[]     18.469M (± 1.6%) i/s -     92.881M in   5.030200s
              Map#[]     15.542M (± 0.0%) i/s -     77.977M in   5.017096s

So YJIT reduces the difference between the two, cc @joeldrapper, and it looks already fast enough without changing anything with YJIT. Surprisingly in that run the numbers look faster before this PR, might be noise or some issue in YJIT, unsure.

eregon commented 1 year ago

Some more measurements, 3.2 with YJIT:

Before:
Warming up --------------------------------------
             Hash#[]     1.646M i/100ms
              Map#[]     1.453M i/100ms
Calculating -------------------------------------
             Hash#[]     17.275M (± 1.0%) i/s -     87.212M in   5.048775s
              Map#[]     16.831M (± 1.2%) i/s -     84.251M in   5.006328s

Warming up --------------------------------------
             Hash#[]     1.647M i/100ms
              Map#[]     1.482M i/100ms
Calculating -------------------------------------
             Hash#[]     17.359M (± 0.6%) i/s -     87.286M in   5.028526s
              Map#[]     16.860M (± 1.0%) i/s -     84.501M in   5.012538s
After:
Warming up --------------------------------------
             Hash#[]     1.674M i/100ms
              Map#[]     1.621M i/100ms
Calculating -------------------------------------
             Hash#[]     19.580M (± 0.6%) i/s -     98.745M in   5.043264s
              Map#[]     16.564M (± 0.3%) i/s -     84.267M in   5.087353s

Warming up --------------------------------------
             Hash#[]     1.649M i/100ms
              Map#[]     1.606M i/100ms
Calculating -------------------------------------
             Hash#[]     19.545M (± 1.3%) i/s -     98.949M in   5.063710s
              Map#[]     16.527M (± 1.6%) i/s -     83.508M in   5.054362s

Interestingly the numbers in Warming up match what I expected. But the numbers Calculating nope, they are weird.