Hi Stuart,

On 06/19/2018 04:09 AM, Stuart Marks wrote:


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.

More a desire than a constraint. If there really is a solution that is better but needs SharedSecrets, then I'm all for it. OTOH, it surely is less maintenance when only public APIs are used, isn't it?

I'd like to use Map.forEach() which is already optimized in most JDK internal implementations while the default implementation is not worse than alternatives.


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).

Right, and my last approach detected that and has thrown ConcurrentModificationException. Which might not be desired in a scenario where a live CHM is copied into an unmodifiable Map for example.


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.

It publishes a Builder object that has a controlled access to the constructing map. You can't get a reference to the constructing map from it and it becomes useless before the constructor of the map returns.


This change also hands out an object that has access to the new Map instance's internals.

Yes, this is the Builder object.

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.

This is not my invention. The same technique is used in ObjectInputStream / ObjectOutputStream.


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.

The technique used in my last webrev and taken from ObjectInputStream / ObjectOutputStream is simple, simple to prove correctness and fast as it needs zero synchronization. It could be called "a single-threaded object" idiom:

class SingleThreadedObject {
    private Thread thread;

    SingleThreadedObject() {
        thread = Thread.currentThread();
    }

    void use() {
        if (Thread.currentThread() != thread) throw new IllegalStateException();

        ... anything ...
    }

    void close() {
        if (Thread.currentThread() != thread) throw new IllegalStateException();
        thread = null;
    }
}


Claim: The thread that constructs the object is the only thread that can use() or close() the object and only until close() is called by the constructing thread. To prove the correctness of this claim, we have consider two scenarios:

- object is use()d and finally close()d in the same thread that constructed it - no need to prove anything here as it is obvious that this works by inspecting the code.

- object is attempted to be use()d or close()d in some other thread from the thread that constructed it.

To prove that such usage is always prevented, we have to ask ourselves what are the possible values of 'thread' field that can be seen by some other thread. There are two possibilities: - null value (uninitialized or already close()d) -> the usage is prevented as null != Thread.currentThread() - non-null value (initialized) -> the usage is prevented as the non-null value is a different thread than current thread which is trying to use() or close() the object

This technique doesn't use any synchronization. Thread.currentThread() is fast (I think I heard that current thread reference or something not far from it is kept in a CPU register all the time).


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.

Not necessarily. Here's another attempt that uses the same technique but relaxes the possible concurrent source map scenario (where number of pairs emitted by Map.forEach() doesn't agree with Map.size()):

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

The construction is moved entirely into a separate MapBuilder object. The builder is used for Map.of(...) methods too.

Here are the benchmark results that show improvements in every respect touched by the patch:

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

Here are the JMH benchmarks used to produce these results:

http://cr.openjdk.java.net/~plevart/jdk-dev/UnmodifiableMap_copyOf/UnmodifiableMapCollectorBench.java
http://cr.openjdk.java.net/~plevart/jdk-dev/UnmodifiableMap_copyOf/UnmodifiableMapCopyOfBench.java
http://cr.openjdk.java.net/~plevart/jdk-dev/UnmodifiableMap_copyOf/UnmodifiableMapOfBench.java


So what do you think of this one?

Regards, Peter

Reply via email to