Proposal: optimization of Map.copyOf and Collectors.toUnmodifiableMap
Stuart Marks
stuart.marks at oracle.com
Tue Jun 19 02:09:42 UTC 2018
On 6/17/18 4:23 PM, Peter Levart wrote:
> My attempt to optimize the Map.copyOf() was also using just public APIs (with
> the addition of an internal class).
> Well, it does speed-up Map.copyOf() by 30%. But I agree there are other
> approaches that could go even further. In particular when all intermediary
> copies could be avoided.
>
> Here's one approach (which still uses just public APIs) and avoids intermediary
> copying:
You mentioned "using just public APIs" a couple times. Are you viewing that as a
constraint? I don't think it is.
> http://cr.openjdk.java.net/~plevart/jdk-dev/UnmodifiableMap_copyOf/webrev.02/
>
> Why choosing Map.forEach for dumping the content of the map instead of iteration
> over entrySet? Because in some Map implementations this is the most direct
> iteration without producing garbage (for example in IdentityHashMap), but in the
> worst case (default Map method for example) it is delegated to iteration over
> entrySet.
Sure, entrySet().toArray() does a lot of allocation and copying. Using forEach()
can avoid this, but it doesn't necessarily preserve the right invariants. In
particular, if the source is a concurrent map, the number of times forEach's
BiConsumer is called might not match size(), if the map has changed size. So,
the forEach approach has to deal with the possibility of it not being called
exactly size() times, whereas we know that the array from toArray() can't change
size (and that its contents are consistent, for some definition of consistent).
This change also publishes a reference to the Map under construction before its
constructor has returned. I'm not sure of all the memory model implications of this.
This change also hands out an object that has access to the new Map instance's
internals. You're aware of this, of course, because you snapshot the current
thread and null it out later, saying
// prevent naughty Map implementations from modifying MapN after
// it is fully constructed
So there are potential security vulnerabilities here, which requires some
serious thought. What you have *seems* reasonable, but my experience is that
things that are subtle but that seem reasonable actually end up having security
holes.
I'm trying to think of some alternatives.
The issue with the forEach() approach on an arbitrary map is that you have to
hand out a BiConsumer, and malicious code can call it even after forEach() has
returned. What's necessary is to have a way to shut off the BiConsumer before
reading out what it's accumulated. I don't know of a simple, fast, and correct
way to do this. (Choose any two!) The "enabled" state (whether a Thread instance
or just a boolean) will probably have to be a volatile that must be checked
every time. What are the performance implications? Anyway, while it seems
promising, the forEach() approach seems like it leads off into the weeds.
Unless you can verify the trustworthiness of the source. Suppose you check the
source map to see if its getClass() == HashMap.class. Then you can be reasonably
assured that forEach() won't misuse the BiConsumer. You can probably also rely
on size() being stable. (Probably also include LinkedHashMap.)
The toUnmodifiableMap collectors can benefit from this if they're changed to use
Map::copyOf, as in your current webrev.
You could also provide a similar fast path for ConcurrentHashMap. It'd have to
be resilient against size changes, but you can trust that it won't misuse the
BiConsumer.
Finally, other map implementations will just have to suffer through the
multi-copy slow path.
s'marks
More information about the core-libs-dev
mailing list