8378895: Reduce object allocations in Renderer.getPeerInstance()#2091
8378895: Reduce object allocations in Renderer.getPeerInstance()#2091mstr2 wants to merge 5 commits intoopenjdk:masterfrom
Conversation
|
👋 Welcome back mstrauss! A progress list of the required criteria for merging this PR into |
|
❗ This change is not yet ready to be integrated. |
Webrevs
|
| new HashMap<>(1); | ||
| private Map<String, EffectPeer> peerCache = | ||
| Collections.synchronizedMap(new HashMap<String, EffectPeer>(5)); | ||
| private static final Map<FilterContext, Renderer> rendererMap = new HashMap<>(1); |
There was a problem hiding this comment.
If this is meant to initialize the map to a size of 1 then HashMap.newHashMap should be used.
| } | ||
| } | ||
|
|
||
| private final Map<PeerCacheKey, EffectPeer> peerCache = Collections.synchronizedMap(new HashMap<>(5)); |
There was a problem hiding this comment.
Isn't a ConcurrentHashMap a better alternative?
There was a problem hiding this comment.
No, I don't think so, and I think Collections.synchronizedMap is also pointless here. If this is supposed to allow concurrent access to the peerCache map, the original implementation is defective: Collections.synchronizedMap clearly states that iteration must be synchronized on the map instance, which this code fails to do.
I've changed the implementation to use a normal HashMap, and synchronize manually where it is accessed or modified. For this purpose, I've removed the Renderer.getPeers() method, which is only called in PPSRenderer.dispose() to iterate over the peers and dispose each one of them. Instead, the operation is now encapsulated in Renderer.clearPeers().
andy-goryachev-oracle
left a comment
There was a problem hiding this comment.
I see several structural changes here (synchronization, changing the order of events around DISPOSED) that go beyond a simple optimization.
I am all for replacing string concatenation with a compound key like object, my main question though is how big of the problem is this? Do we really incur significant overhead creating those keys?
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| return o instanceof PeerCacheKey other |
There was a problem hiding this comment.
typically
if(o == this) {
return true;
}
There was a problem hiding this comment.
Yes, but since the lookup key is never inserted into the map, o is never this. That would be an optimization that'll never be hit.
There was a problem hiding this comment.
makes sense. maybe add a comment then?
| public final EffectPeer getPeerInstance(FilterContext fctx, String name, int unrollCount) { | ||
| synchronized (peerCache) { | ||
| // The lookup key is only used to look up mappings, it is never inserted into the map. | ||
| peerCacheKey.name = name; |
There was a problem hiding this comment.
this code is not re-entrant. can you guarantee it's safe?
There was a problem hiding this comment.
To be fair, I don't know why the code here is synchronized at all. I only ever see this method be called on the renderer thread (as it should be). However, I don't think that I've changed anything substantial here: the method was synchronized before, and is synchronized now.
There was a problem hiding this comment.
Hm, I meant something else: you are changing a peerCacheKey instance, are you sure it will never re-enter the same method (and clobber the same instance?)
You've also added synchronization on a different object here, which might create a deadlock. And you do bring another good point - if this code is always called from the same thread, why synchronize at all?
There was a problem hiding this comment.
The reusable peerCacheKey instance is only used in these three lines:
peerCacheKey.name = name;
peerCacheKey.unrollCount = unrollCount;
EffectPeer<?> peer = peerCache.get(peerCacheKey);Since the Map.get() method won't call out to other code, I don't think there is any chance that something can go wrong in this particular segment of code.
You've also added synchronization on a different object here, which might create a deadlock.
I don't see any potential problem that wasn't there before. Previously, this method synchronized on this, now it synchronizes on peerCache. That's not a material difference in itself, considering that there's only one other place where we synchronize on peerCache, and I've made sure that it won't call out to other methods.
| protected final void clearPeers() { | ||
| EffectPeer<?>[] peers; | ||
|
|
||
| synchronized (peerCache) { |
There was a problem hiding this comment.
Having two monitors involved (peerCache here and peer's one elsewhere) might lead to a deadlock. Just by looking at the code, I see one potential with createPeer() in Renderer:273 . Could you take a look please?
There was a problem hiding this comment.
I'm not calling any dangerous method while holding a lock here. The code first copies the values of the map into a local array, then releases the lock, and only then calls EffectPeer.dispose().
There was a problem hiding this comment.
Right, my original comment should have been placed at L237, where inside a block synchronized on peerCache, you are calling createPeer(). It looks like the latter is not synchronized on anything (can't see past reflection though), but if inside of createPeer() there is another code synchronized around a different object, there might be deadlock.
It looks like there isn't, but my question remains: why do you want to introduce these non-trivial changes in the first place, how big of a problem is this allocation is?
There was a problem hiding this comment.
I want to make these changes to improve the code. I don't really see the problem here...?
As always with these kinds of optimizations: it depends on the application. Generally, you shouldn't expect this particular issue to be noticeable. But when you have 1000 of these "not generally noticeable" small issues, quantity can become a quality of its own. |
The current implementation of
Renderer.getPeerInstance()looks up mappings by concatenating strings to form a combined key:This can be improved by not using strings to store the combined key, but using a simple
PeerCacheKeyclass instead.The
Rendererclass then uses a single reusablePeerCacheKeyobject to look up mappings, making the lookup allocation-free./reviewers 2
Progress
Issue
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/2091/head:pull/2091$ git checkout pull/2091Update a local copy of the PR:
$ git checkout pull/2091$ git pull https://git.openjdk.org/jfx.git pull/2091/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 2091View PR using the GUI difftool:
$ git pr show -t 2091Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/2091.diff
Using Webrev
Link to Webrev Comment