Skip to content

Commit 5357e9a

Browse files
committed
[GR-19220] Add Java CHM implementation for TruffleRuby::ConcurrentHashMap (#2339)
PullRequest: truffleruby/2636
2 parents 74aac51 + ae5787f commit 5357e9a

File tree

13 files changed

+760
-0
lines changed

13 files changed

+760
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
New features:
44

5+
* New `TruffleRuby::ConcurrentMap` data structure for use in [`concurrent-ruby`](https://github.com/ruby-concurrency/concurrent-ruby) (#2339, @wildmaples).
56

67
Bug fixes:
78

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. This
2+
# code is released under a tri EPL/GPL/LGPL license. You can use it,
3+
# redistribute it and/or modify it under the terms of the:
4+
#
5+
# Eclipse Public License version 2.0, or
6+
# GNU General Public License version 2, or
7+
# GNU Lesser General Public License version 2.1.
8+
9+
if RUBY_ENGINE == 'truffleruby'
10+
map = TruffleRuby::ConcurrentMap.new
11+
10_000.times { |i| map[i] = i }
12+
map[:a] = 1
13+
14+
benchmark 'TruffleRuby::ConcurrentMap#[]' do
15+
map[:a]
16+
end
17+
18+
benchmark 'TruffleRuby::ConcurrentMap#[]=' do
19+
map[:set] = :value
20+
end
21+
22+
benchmark 'TruffleRuby::ConcurrentMap#each_pair' do
23+
map.each_pair { |k,v| k }
24+
end
25+
26+
benchmark 'TruffleRuby::ConcurrentMap#delete' do
27+
map[:to_delete] = true
28+
map.delete(:to_delete)
29+
end
30+
end

doc/user/truffleruby-additions.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,44 @@ TruffleRuby provides these non-standard methods and classes that provide additio
5252

5353
* `AtomicReference` is marshalable.
5454

55+
### Concurrent Maps
56+
57+
`TruffleRuby::ConcurrentMap` is a key-value data structure, like a `Hash` and using `#hash` and `#eql?` to compare keys and identity to compare values. Unlike `Hash` it is unordered. All methods on `TruffleRuby::ConcurrentMap` are thread-safe but should have higher concurrency than a fully syncronized implementation. It is intended to be used by gems such as [`concurrent-ruby`](https://github.com/ruby-concurrency/concurrent-ruby) - please use via this gem rather than using directly.
58+
59+
* `map = TruffleRuby::ConcurrentMap.new([initial_capacity: ...], [load_factor: ...])`
60+
61+
* `map[key] = new_value`
62+
63+
* `map[key]`
64+
65+
* `map.compute_if_absent(key) { computed_value }` if the key is not found, run the block and store the result. The block is run at most once. Returns the computed value.
66+
67+
* `map.compute_if_present(key) { |current_value| computed_value }` if the key is found, run the block and store the result. If the block returns `nil` the entry for that key is removed. The block is run at most once. Returns the final value, or `nil` if the block returned `nil`.
68+
69+
* `map.compute(key) { |current_value| computed_value }` run the block, passing the current value if there is one or `nil`, and store the result. If the block returns `nil` the entry for that key is removed. Returns the computed value.
70+
71+
* `map.merge_pair(key, new_value) { |existing_value| merged_value }` if key is not found or is `nil`, store the new value, otherwise call the block and store the result, or remove the entry if the block returns `nil`. Returns the final value for that entry, or `nil` if the block returned `nil`.
72+
73+
* `map.replace_pair(key, expected_value, new_value)` replace the value for key but only if the existing value for it is the same as `expected_value` (compared by identity). Returns if the value was replaced or not.
74+
75+
* `map.replace_if_exists(key, value)` replace the value for key but only if it was found. Returns `value` if the key exists or `nil`.
76+
77+
* `map.get_and_set(key, new_value)` sets the value for a key and returns the previous value.
78+
79+
* `map.key?(key)` returns if a key is in the map.
80+
81+
* `map.delete(key)` removes a key from the map if it exists, returning the value or `nil` if it did not exist.
82+
83+
* `map.delete_pair(key, expected_value)` removes a key but only if the existing value for it is the same as `expected_value` (compared by identity). Returns if the key was deleted.
84+
85+
* `map.clear` removes all entries from the map.
86+
87+
* `map.size` gives the number of entries in the map.
88+
89+
* `map.get_or_default(key, default_value)`
90+
91+
* `map.each_pair { |key, value| ... }`
92+
5593
## FFI
5694

5795
TruffleRuby includes a [Ruby-FFI](https://github.com/ffi/ffi) backend. This should be transparent: you can just install the `ffi` gem as normal, and it will use TruffleRuby's FFI backend. TruffleRuby also includes a default version of the FFI gem, so `require "ffi"` always works on TruffleRuby, even if the gem is not installed.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
require_relative '../../ruby/spec_helper'
2+
3+
describe "TruffleRuby::ConcurrentMap" do
4+
before do
5+
@h = TruffleRuby::ConcurrentMap.new
6+
end
7+
8+
it "#[] of a new instance is empty" do
9+
@h[:empty].should.equal? nil
10+
end
11+
12+
it "#[]= creates a new key value pair" do
13+
new_value = "bar"
14+
@h[:foo] = new_value
15+
@h[:foo].should.equal? new_value
16+
end
17+
18+
it "#compute_if_absent computes and stores new value for key if key is absent" do
19+
expected_value = "value for foobar"
20+
@h.compute_if_absent(:foobar) { expected_value }.should.equal? expected_value
21+
22+
@h[:foobar].should.equal? expected_value
23+
end
24+
25+
it "#compute_if_present computes and stores new value for key if key is present" do
26+
expected_value = "new value"
27+
@h[:foobar] = "old value"
28+
@h.compute_if_present(:foobar) { expected_value }.should.equal? expected_value
29+
30+
@h[:foobar].should.equal? expected_value
31+
end
32+
33+
it "#compute computes and stores new value" do
34+
expected_value = "new value"
35+
@h[:foobar] = "old value"
36+
@h.compute(:foobar) { expected_value }.should.equal? expected_value
37+
38+
@h[:foobar].should.equal? expected_value
39+
end
40+
41+
it "#merge_pair stores value if key is absent" do
42+
new_value = "bloop"
43+
@h.merge_pair(:foobar, new_value) do |value|
44+
value + new_value
45+
end.should.equal? new_value
46+
@h[:foobar].should.equal? new_value
47+
end
48+
49+
it "#merge_pair stores computed value if key is present" do
50+
old_value, new_value = "bleep", "bloop"
51+
expected_value = old_value + new_value
52+
@h[:foobar] = old_value
53+
54+
@h.merge_pair(:foobar, new_value) do |value|
55+
value + new_value
56+
end.should == expected_value
57+
@h[:foobar].should == expected_value
58+
end
59+
60+
it "#replace_pair replaces old value with new value if key exists and current value matches old value" do
61+
old_value, new_value = "bleep", "bloop"
62+
@h[:foobar] = old_value
63+
64+
@h.replace_pair(:foobar, old_value, new_value).should == true
65+
@h[:foobar].should.equal? new_value
66+
end
67+
68+
it "#replace_pair replaces the entry if the old value is a primitive" do
69+
one_as_long = (1 << 48) / (1 << 48)
70+
Truffle::Debug.java_class_of(one_as_long).should == 'Long'
71+
one_as_int = 1
72+
Truffle::Debug.java_class_of(one_as_int).should == 'Integer'
73+
74+
@h[:foobar] = one_as_long
75+
76+
@h.replace_pair(:foobar, one_as_int, 2).should == true
77+
@h[:foobar].should == 2
78+
end
79+
80+
it "#replace_pair doesn't replace old value if current value doesn't match old value" do
81+
expected_old_value = "BLOOP"
82+
@h[:foobar] = expected_old_value
83+
84+
@h.replace_pair(:foobar, "bleep", "bloop").should == false
85+
@h[:foobar].should.equal? expected_old_value
86+
end
87+
88+
it "#replace_if_exists replaces value if key exists" do
89+
@h[:foobar] = "bloop"
90+
expected_value = "bleep"
91+
92+
@h.replace_if_exists(:foobar, expected_value).should == "bloop"
93+
@h[:foobar].should.equal? expected_value
94+
end
95+
96+
it "#get_and_set gets current value and set new value" do
97+
@h[:foobar] = "bloop"
98+
expected_value = "bleep"
99+
100+
@h.get_and_set(:foobar, expected_value).should == "bloop"
101+
@h[:foobar].should.equal? expected_value
102+
end
103+
104+
it "#key? returns true if key is present" do
105+
@h[:foobar] = "bloop"
106+
@h.key?(:foobar).should == true
107+
end
108+
109+
it "#key? returns false if key is absent" do
110+
@h.key?(:foobar).should == false
111+
end
112+
113+
it "#delete deletes key and value pair" do
114+
value = "bloop"
115+
@h[:foobar] = value
116+
@h.delete(:foobar).should.equal? value
117+
@h[:foobar].should == nil
118+
end
119+
120+
it "#delete_pair deletes pair if value equals provided value" do
121+
value = "bloop"
122+
@h[:foobar] = value
123+
@h.delete_pair(:foobar, value).should == true
124+
@h[:foobar].should == nil
125+
end
126+
127+
it "#delete_pair deletes pair if the old value is a primitive" do
128+
one_as_long = (1 << 48) / (1 << 48)
129+
Truffle::Debug.java_class_of(one_as_long).should == 'Long'
130+
one_as_int = 1
131+
Truffle::Debug.java_class_of(one_as_int).should == 'Integer'
132+
133+
@h[:foobar] = one_as_long
134+
135+
@h.delete_pair(:foobar, one_as_int).should == true
136+
@h[:foobar].should == nil
137+
end
138+
139+
it "#delete_pair doesn't delete pair if value equals provided value" do
140+
value = "bloop"
141+
@h[:foobar] = value
142+
@h.delete_pair(:foobar, "BLOOP").should == false
143+
@h[:foobar].should.equal? value
144+
end
145+
146+
it "#clear returns an empty hash" do
147+
@h[:foobar] = "bleep"
148+
@h.clear
149+
@h.key?(:foobar).should == false
150+
@h.size.should == 0
151+
end
152+
153+
it "#size returns the size of hash" do
154+
@h[:foobar], @h[:barfoo] = "bleep", "bloop"
155+
@h.size.should == 2
156+
end
157+
158+
it "#get_or_default returns value of key if key mapped" do
159+
@h[:foobar] = "bleep"
160+
@h.get_or_default(:foobar, "BLEEP").should == "bleep"
161+
@h.key?(:foobar).should == true
162+
end
163+
164+
it "#get_or_default returns default if key isn't mapped" do
165+
@h.get_or_default(:foobar, "BLEEP").should == "BLEEP"
166+
@h.key?(:foobar).should == false
167+
end
168+
169+
it "#each_pair passes each key value pair to given block" do
170+
@h[:foobar], @h[:barfoo] = "bleep", "bloop"
171+
@h.each_pair do |key, value|
172+
value.should == @h[key]
173+
end
174+
end
175+
176+
it "#each_pair returns self" do
177+
@h.each_pair { }.should.equal?(@h)
178+
end
179+
180+
it "#initialize_copy creates a new instance" do
181+
@h.should_not.equal? @h.dup
182+
end
183+
end

src/main/java/org/truffleruby/RubyLanguage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
import org.truffleruby.core.time.RubyTime;
8383
import org.truffleruby.core.tracepoint.RubyTracePoint;
8484
import org.truffleruby.extra.RubyAtomicReference;
85+
import org.truffleruby.extra.RubyConcurrentMap;
8586
import org.truffleruby.extra.ffi.RubyPointer;
8687
import org.truffleruby.core.string.ImmutableRubyString;
8788
import org.truffleruby.language.NotProvided;
@@ -195,6 +196,7 @@ public final class RubyLanguage extends TruffleLanguage<RubyContext> {
195196
public final Shape atomicReferenceShape = createShape(RubyAtomicReference.class);
196197
public final Shape bindingShape = createShape(RubyBinding.class);
197198
public final Shape byteArrayShape = createShape(RubyByteArray.class);
199+
public final Shape concurrentMapShape = createShape(RubyConcurrentMap.class);
198200
public final Shape conditionVariableShape = createShape(RubyConditionVariable.class);
199201
public final Shape customRandomizerShape = createShape(RubyCustomRandomizer.class);
200202
public final Shape digestShape = createShape(RubyDigest.class);

src/main/java/org/truffleruby/builtins/BuiltinsClasses.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@
140140
import org.truffleruby.debug.TruffleDebugNodesFactory;
141141
import org.truffleruby.extra.AtomicReferenceNodesBuiltins;
142142
import org.truffleruby.extra.AtomicReferenceNodesFactory;
143+
import org.truffleruby.extra.ConcurrentMapNodesBuiltins;
144+
import org.truffleruby.extra.ConcurrentMapNodesFactory;
143145
import org.truffleruby.extra.TruffleGraalNodesBuiltins;
144146
import org.truffleruby.extra.TruffleGraalNodesFactory;
145147
import org.truffleruby.extra.TrufflePosixNodesBuiltins;
@@ -182,6 +184,7 @@ public static void setupBuiltinsLazy(CoreMethodNodeManager coreManager) {
182184
ByteArrayNodesBuiltins.setup(coreManager);
183185
CExtNodesBuiltins.setup(coreManager);
184186
ClassNodesBuiltins.setup(coreManager);
187+
ConcurrentMapNodesBuiltins.setup(coreManager);
185188
ConditionVariableNodesBuiltins.setup(coreManager);
186189
CoverageNodesBuiltins.setup(coreManager);
187190
CustomRandomizerNodesBuiltins.setup(coreManager);
@@ -263,6 +266,7 @@ public static void setupBuiltinsLazyPrimitives(PrimitiveManager primitiveManager
263266
CExtNodesBuiltins.setupPrimitives(primitiveManager);
264267
ClassNodesBuiltins.setupPrimitives(primitiveManager);
265268
CustomRandomizerNodesBuiltins.setupPrimitives(primitiveManager);
269+
ConcurrentMapNodesBuiltins.setupPrimitives(primitiveManager);
266270
ConditionVariableNodesBuiltins.setupPrimitives(primitiveManager);
267271
CoverageNodesBuiltins.setupPrimitives(primitiveManager);
268272
DigestNodesBuiltins.setupPrimitives(primitiveManager);
@@ -343,6 +347,7 @@ public static List<List<? extends NodeFactory<? extends RubyBaseNode>>> getCoreN
343347
ByteArrayNodesFactory.getFactories(),
344348
CExtNodesFactory.getFactories(),
345349
ClassNodesFactory.getFactories(),
350+
ConcurrentMapNodesFactory.getFactories(),
346351
ConditionVariableNodesFactory.getFactories(),
347352
CoverageNodesFactory.getFactories(),
348353
CustomRandomizerNodesFactory.getFactories(),

src/main/java/org/truffleruby/collections/ConcurrentOperations.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
*/
1010
package org.truffleruby.collections;
1111

12+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
13+
1214
import java.util.Map;
1315
import java.util.concurrent.ConcurrentHashMap;
1416
import java.util.function.Function;
@@ -22,6 +24,7 @@ public abstract class ConcurrentOperations {
2224
* #computeIfAbsent() is used if the key cannot be found with {@link Map#get(Object)}, and therefore this method has
2325
* the same semantics as the passed map's #computeIfAbsent(). Notably, for ConcurrentHashMap, the lambda is
2426
* guaranteed to be only executed once per missing key. */
27+
@TruffleBoundary
2528
public static <K, V> V getOrCompute(Map<K, V> map, K key, Function<? super K, ? extends V> compute) {
2629
V value = map.get(key);
2730
if (value != null) {

src/main/java/org/truffleruby/core/CoreLibrary.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ public CoreLibrary(RubyContext context, RubyLanguage language) {
453453
"Converter");
454454
final RubyModule truffleRubyModule = defineModule("TruffleRuby");
455455
defineClass(truffleRubyModule, objectClass, "AtomicReference");
456+
defineClass(truffleRubyModule, objectClass, "ConcurrentMap");
456457
truffleModule = defineModule("Truffle");
457458
truffleInternalModule = defineModule(truffleModule, "Internal");
458459
graalErrorClass = defineClass(truffleModule, exceptionClass, "GraalError");
@@ -976,6 +977,7 @@ public boolean isTruffleBootMainMethod(SharedMethodInfo info) {
976977
"/core/kernel.rb",
977978
"/core/lazy_rubygems.rb",
978979
"/core/truffle/boot.rb",
980+
"/core/truffle/concurrent_map.rb",
979981
"/core/truffle/debug.rb",
980982
"/core/truffle/diggable.rb",
981983
"/core/truffle/encoding_operations.rb",

src/main/java/org/truffleruby/core/kernel/KernelNodes.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ public static SameOrEqlNode create() {
230230
return KernelNodesFactory.SameOrEqlNodeGen.create();
231231
}
232232

233+
public static SameOrEqlNode getUncached() {
234+
return KernelNodesFactory.SameOrEqlNodeGen.getUncached();
235+
}
236+
233237
public abstract boolean execute(Object a, Object b);
234238

235239
@Specialization(guards = "referenceEqual.executeReferenceEqual(a, b)")

0 commit comments

Comments
 (0)