Hi Peter,

Finally getting back to this.

I think there's clearly room for improvement in the creation of the unmodifiable collections. Right now the creation paths (mostly) use only public APIs, which can result in unnecessary allocation and copying. Certainly Map.copyOf does this, but there are also other cases as well.

For copying from an unknown map, there are a couple approaches:

* accumulate keys and values in a single ArrayList, which can be presized as necessary, but which will grow if necessary; then copy elements from a subrange of ArrayList's internal array (similar to your MapN(Object[], len) overload)

* accumulate keys and values into a SpinedBuffer, which doesn't require copying to grow, which is preferable if for some reason we can't pre-size accurately; and then copy the elements out of it

The Collectors.toUnmodifiableMap implementations are known to create HashMap instances, so they could pass the HashMap directly to a private MapN constructor that in turn could talk directly to HashMap to get the keys and values. This avoids allocation of a full-sized buffer and one copy.

Note that these techniques involve creating new interfaces, sometimes that cross package boundaries. It's a bit of an irritant to have to plumb new paths that go through SharedSecrets, but it seems likely to be worthwhile if we can avoid bulk allocation and copying steps.

Given these, it doesn't seem to me that the BiBuffer approach helps very much. I think there are many other avenues that would be worthwhile to explore, and that possibly can provide bigger savings.

s'marks





On 6/11/18 3:57 AM, Peter Levart wrote:
Hi,

Those two methods were added in JDK 10 and they are not very optimal. Map.copyOf(Map map) 1st dumps the source map into an array of Map.Entry(s) (map.entrySet().toArray(new Entry[0])), which typically creates new Map.Entry objects, then passes the array to Map.ofEntries(Map.Entry[] entries) factory method that iterates the array and constructs a key/value interleaved array from it which is passed to ImmutableCollections.MapN constructor, which then constructs a linear-probing hash table from it. So each key and value is copied 3 times, while several temporary objects are created in the process.

One copying step could be eliminated and construction of temporary Map.Entry objects too:

http://cr.openjdk.java.net/~plevart/jdk-dev/UnmodifiableMap_copyOf/webrev.01/

Collecting stream(s) using Collectors.toUnmodifiableMap() 1st collects key/value pairs into a HashMap, then it performs equivalent operations as Map.copyOf(hashMap) at the end. Using Map.copyOf() directly benefits this collection operation too.

The following benchmark:

http://cr.openjdk.java.net/~plevart/jdk-dev/UnmodifiableMap_copyOf/UnmodifiableMapBench.java


Shows up to 30% improvement of .copyOf operation with this patch applied:

Original:

Benchmark                               (size)  Mode Cnt      Score Error  Units
UnmodifiableMapBench.copyOf                 10  avgt 10    403.633 ± 2.640  
ns/op
UnmodifiableMapBench.copyOf                100  avgt   10 3489.623 ± 44.590  
ns/op
UnmodifiableMapBench.copyOf               1000  avgt   10 40030.572 ± 277.075 ns/op
UnmodifiableMapBench.toUnmodifiableMap      10  avgt 10    831.221 ± 3.816  
ns/op
UnmodifiableMapBench.toUnmodifiableMap     100  avgt   10 9783.519 ± 43.097  
ns/op
UnmodifiableMapBench.toUnmodifiableMap    1000  avgt   10 96524.536 ± 670.818 ns/op

Patched:

Benchmark                               (size)  Mode Cnt      Score Error  Units
UnmodifiableMapBench.copyOf                 10  avgt 10    264.172 ± 1.882  
ns/op
UnmodifiableMapBench.copyOf                100  avgt   10 2318.974 ± 15.877  
ns/op
UnmodifiableMapBench.copyOf               1000  avgt   10 29291.782 ± 3139.737 ns/op
UnmodifiableMapBench.toUnmodifiableMap      10  avgt 10    771.221 ± 65.432  
ns/op
UnmodifiableMapBench.toUnmodifiableMap     100  avgt   10 9275.016 ± 725.722  
ns/op
UnmodifiableMapBench.toUnmodifiableMap    1000  avgt   10 82204.342 ± 851.741 ns/op


Production of garbage is also reduced, since no Map.Entry temporary objects are constructed:

Original:

Benchmark (size)  Mode  Cnt      Score       Error   Units
UnmodifiableMapBench.copyOf:·gc.alloc.rate.norm 10  avgt   10    416.001 ± 0.002    B/op UnmodifiableMapBench.copyOf:·gc.alloc.rate.norm 100  avgt   10 2936.005 ± 0.019    B/op UnmodifiableMapBench.copyOf:·gc.alloc.rate.norm 1000  avgt   10 28136.059 ± 0.199    B/op UnmodifiableMapBench.toUnmodifiableMap:·gc.alloc.rate.norm 10  avgt 10 1368.001 ±     0.004    B/op UnmodifiableMapBench.toUnmodifiableMap:·gc.alloc.rate.norm 100  avgt 10 10208.139 ±     0.045    B/op UnmodifiableMapBench.toUnmodifiableMap:·gc.alloc.rate.norm 1000  avgt 10 93025.923 ±     0.573    B/op

Patched:

Benchmark (size)  Mode  Cnt      Score       Error   Units
UnmodifiableMapBench.copyOf:·gc.alloc.rate.norm 10  avgt   10    304.000 ± 0.001    B/op UnmodifiableMapBench.copyOf:·gc.alloc.rate.norm 100  avgt   10 2464.004 ± 0.012    B/op UnmodifiableMapBench.copyOf:·gc.alloc.rate.norm 1000  avgt   10 24064.040 ± 0.137    B/op UnmodifiableMapBench.toUnmodifiableMap:·gc.alloc.rate.norm 10  avgt 10 1256.001 ±     0.003    B/op UnmodifiableMapBench.toUnmodifiableMap:·gc.alloc.rate.norm 100  avgt 10 9720.153 ±     0.055    B/op UnmodifiableMapBench.toUnmodifiableMap:·gc.alloc.rate.norm 1000  avgt 10 88905.688 ±     0.574    B/op


So what do you think? Is this an improvement?

Regards, Peter

Reply via email to