Skip to content

Commit ebf63ce

Browse files
committed
Optimize Concurrent::Map#[] on CRuby by letting the backing Hash handle the default_proc
* 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
1 parent fa9c790 commit ebf63ce

File tree

5 files changed

+48
-43
lines changed

5 files changed

+48
-43
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Current
22

3+
* (#989) Optimize `Concurrent::Map#[]` on CRuby by letting the backing Hash handle the `default_proc`.
4+
35
## Release v1.2.0 (23 Jan 2023)
46

57
* (#962) Fix ReentrantReadWriteLock to use the same granularity for locals as for Mutex it uses.

lib/concurrent-ruby/concurrent/collection/map/mri_map_backend.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ module Collection
99
# @!visibility private
1010
class MriMapBackend < NonConcurrentMapBackend
1111

12-
def initialize(options = nil)
13-
super(options)
12+
def initialize(options = nil, &default_proc)
13+
super(options, &default_proc)
1414
@write_lock = Mutex.new
1515
end
1616

lib/concurrent-ruby/concurrent/collection/map/non_concurrent_map_backend.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ class NonConcurrentMapBackend
1212
# directly without calling each other. This is important because of the
1313
# SynchronizedMapBackend which uses a non-reentrant mutex for performance
1414
# reasons.
15-
def initialize(options = nil)
16-
@backend = {}
15+
def initialize(options = nil, &default_proc)
16+
validate_options_hash!(options) if options.kind_of?(::Hash)
17+
@backend = Hash.new(&default_proc)
18+
@default_proc = default_proc
1719
end
1820

1921
def [](key)
@@ -55,7 +57,7 @@ def compute_if_present(key)
5557
end
5658

5759
def compute(key)
58-
store_computed_value(key, yield(@backend[key]))
60+
store_computed_value(key, yield(get_or_default(key, nil)))
5961
end
6062

6163
def merge_pair(key, value)
@@ -67,7 +69,7 @@ def merge_pair(key, value)
6769
end
6870

6971
def get_and_set(key, value)
70-
stored_value = @backend[key]
72+
stored_value = get_or_default(key, nil)
7173
@backend[key] = value
7274
stored_value
7375
end
@@ -109,13 +111,11 @@ def get_or_default(key, default_value)
109111
@backend.fetch(key, default_value)
110112
end
111113

112-
alias_method :_get, :[]
113-
alias_method :_set, :[]=
114-
private :_get, :_set
115114
private
115+
116116
def initialize_copy(other)
117117
super
118-
@backend = {}
118+
@backend = Hash.new(&@default_proc)
119119
self
120120
end
121121

lib/concurrent-ruby/concurrent/map.rb

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ class Map < Collection::MapImplementation
4646
# @note Atomic methods taking a block do not allow the `self` instance
4747
# to be used within the block. Doing so will cause a deadlock.
4848

49+
# @!method []=(key, value)
50+
# Set a value with key
51+
# @param [Object] key
52+
# @param [Object] value
53+
# @return [Object] the new value
54+
4955
# @!method compute_if_absent(key)
5056
# Compute and store new value for key if the key is absent.
5157
# @param [Object] key
@@ -119,41 +125,38 @@ class Map < Collection::MapImplementation
119125
# @return [true, false] true if deleted
120126
# @!macro map.atomic_method
121127

122-
#
123-
def initialize(options = nil, &block)
124-
if options.kind_of?(::Hash)
125-
validate_options_hash!(options)
126-
else
127-
options = nil
128-
end
128+
# NonConcurrentMapBackend handles default_proc natively
129+
unless defined?(Collection::NonConcurrentMapBackend) and self < Collection::NonConcurrentMapBackend
129130

130-
super(options)
131-
@default_proc = block
132-
end
131+
# @param [Hash, nil] options options to set the :initial_capacity or :load_factor. Ignored on some Rubies.
132+
# @param [Proc] default_proc Optional block to compute the default value if the key is not set, like `Hash#default_proc`
133+
def initialize(options = nil, &default_proc)
134+
if options.kind_of?(::Hash)
135+
validate_options_hash!(options)
136+
else
137+
options = nil
138+
end
133139

134-
# Get a value with key
135-
# @param [Object] key
136-
# @return [Object] the value
137-
def [](key)
138-
if value = super # non-falsy value is an existing mapping, return it right away
139-
value
140-
# re-check is done with get_or_default(key, NULL) instead of a simple !key?(key) in order to avoid a race condition, whereby by the time the current thread gets to the key?(key) call
141-
# a key => value mapping might have already been created by a different thread (key?(key) would then return true, this elsif branch wouldn't be taken and an incorrent +nil+ value
142-
# would be returned)
143-
# note: nil == value check is not technically necessary
144-
elsif @default_proc && nil == value && NULL == (value = get_or_default(key, NULL))
145-
@default_proc.call(self, key)
146-
else
147-
value
140+
super(options)
141+
@default_proc = default_proc
148142
end
149-
end
150143

151-
# Set a value with key
152-
# @param [Object] key
153-
# @param [Object] value
154-
# @return [Object] the new value
155-
def []=(key, value)
156-
super
144+
# Get a value with key
145+
# @param [Object] key
146+
# @return [Object] the value
147+
def [](key)
148+
if value = super # non-falsy value is an existing mapping, return it right away
149+
value
150+
# re-check is done with get_or_default(key, NULL) instead of a simple !key?(key) in order to avoid a race condition, whereby by the time the current thread gets to the key?(key) call
151+
# a key => value mapping might have already been created by a different thread (key?(key) would then return true, this elsif branch wouldn't be taken and an incorrent +nil+ value
152+
# would be returned)
153+
# note: nil == value check is not technically necessary
154+
elsif @default_proc && nil == value && NULL == (value = get_or_default(key, NULL))
155+
@default_proc.call(self, key)
156+
else
157+
value
158+
end
159+
end
157160
end
158161

159162
alias_method :get, :[]

spec/concurrent/map_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -777,8 +777,8 @@ def key # assert_collision_resistance expects to be able to call .key to get the
777777
end
778778
expect_no_size_change cache do
779779
expect_size_change 1, dupped do
780-
expect(:default_value).to eq dupped[:d]
781-
expect(false).to eq cache.key?(:d)
780+
expect(dupped[:d]).to eq :default_value
781+
expect(cache.key?(:d)).to eq false
782782
end
783783
end
784784
end

0 commit comments

Comments
 (0)