Optimize MemoryPool synchronization#14117
Optimize MemoryPool synchronization#14117viczhang861 wants to merge 1 commit intoprestodb:masterfrom
Conversation
|
Do we know today when are we using tagged memory? :)
@viczhang861 : That's a nice finding! I am wondering if you have any profiling numbers to share? (e.g. asyncprofiler supports profiling lock contention by using |
|
A different approach would be avoid synchronize the whole Alternatively, we can make this being able to enable/disable at a per-query basis, so we can make it default to false and only enable it when we want to debug? -- This might be easier to implement. What do you think? @arhimondr , @aweisberg , @viczhang861 |
|
|
The lock is already held from all the contexts where tagged memory is updated. So the # of lock acquisitions isn't changed although the duration is probably changed because it's two less map updates. Since the lock is still being acquired does removing tagged allocations improve the situation much? I think this can be made faster in better and still simple ways.
We could roll revocable reservations into the same thing. We also should use mutable long wrappers instead of Long so we don't have to keep allocating under the lock when updating the tagged memory allocations. The next thing would be could we update tagged memory allocations outside the lock? How many things there can we push outside the lock? Need to spend more time looking at it to determine that. |
|
Also RE eliminating the lock entirely, not sure. It's a pretty complicated set of interactions between moving memory between pools and managing the future that notifies waiters that memory is available. |
Yes, it is mainly about making the function run faster with less duration |
c75c0e5 to
e2102cb
Compare
presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java
Outdated
Show resolved
Hide resolved
aweisberg
left a comment
There was a problem hiding this comment.
I think free() is not safe in this formulation. I also don't think move Is safe even though it is synchronized, but not synchronized with the rest of the code manipulating these maps.
Read write locks are generally kind of slow but it might work to read lock for your basic free and reserve. Write lock for ones when you want to remove an entries from the map, and move between threadpools.
From a correctness standpoint I think that is the simplest path forward because it allows us to separate things we think can be correct when done concurrently from things where we require full isolation.
We already acquire a lock per operation and aren't making it truly lock free so the RW lock should be fine. I don't recall if RW locks perform the same as regular locks.
The truly lock free solution would be to immutably CAS from one state to another on a per query basis, but there are several fields we are updating at once including large nested maps so I don't think it's viable here.
e2102cb to
005032a
Compare
I agree. I rewrite previous logics to make free() thread safe without synchronizing
|
656ed0d to
8bd336e
Compare
There was a problem hiding this comment.
Can you update this to be like reserve? reserving revocable shouldn't be slow when we improved regular reserve. We have the knowledge and experience right now to do it correctly and it will be harder and more confusing for someone coming in later to try and figure out why it's different and how to safely improve it.
There was a problem hiding this comment.
Don't call containsKey, switch between merge vs computeIfPresent. Avoids repeating the map lookup.
There was a problem hiding this comment.
Optimized with calling get(key) at most once.
presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java
Outdated
Show resolved
Hide resolved
presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java
Outdated
Show resolved
Hide resolved
8bd336e to
6888976
Compare
There was a problem hiding this comment.
This still doesn't match how regular free works. This unconditional put overwrites the value another thread has put in. I think it needs to basically match what we did in free in terms of merging in the initial -bytes. Then you can remove the "else".
There was a problem hiding this comment.
I removed old read and then remove, and use the strategy in free
6888976 to
538322f
Compare
538322f to
2b679e5
Compare
presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java
Outdated
Show resolved
Hide resolved
- Use ConcurrentHashMap - Remove unnecessary synchornization of instance method - Synchronization is needed for updating reservedBytes - Synchronization is needed for adding/removing queryId
2b679e5 to
469bf7d
Compare
| private long reservedBytes; | ||
| @GuardedBy("this") | ||
| private long reservedRevocableBytes; | ||
| private volatile long reservedBytes; |
There was a problem hiding this comment.
Does it make sense to mark it with @GuardedBy("this")? cc @aweisberg , @arhimondr
wenleix
left a comment
There was a problem hiding this comment.
LGTM % a minor question.
| } | ||
| } | ||
| } | ||
| synchronized (this) { |
There was a problem hiding this comment.
why this now needs to be synchronized?
There was a problem hiding this comment.
The idea is to use synchronization whenever write operation is performed, and let read operation become lock free. Thus, reading is not @GuardedBy("this")
| updateTaggedMemoryAllocations(queryId, allocationTag, bytes); | ||
| } | ||
| if (bytes != 0) { | ||
| while (true) { |
There was a problem hiding this comment.
i understand what this tries to do (basically either one of the following operation succeed, we are good). But it's a bit mind-twisting :). is this a common practice Java concurrency programming? ;) cc @aweisberg , @arhimondr
| @GuardedBy("this") | ||
| // TODO: It would be better if we just tracked QueryContexts, but their lifecycle is managed by a weak reference, so we can't do that | ||
| private final Map<QueryId, Long> queryMemoryReservations = new HashMap<>(); | ||
| private final Map<QueryId, Long> queryMemoryReservations = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
All three maps (queryMemoryReservations, taggedMemoryAllocations, queryMemoryRevocableReservations) are here to track memory reservations per query. These maps are quite orthogonal to the main function of the MemoryPool class. Instead of trying to optimize locking here by applying various "ninja" techniques i would strongly recommend moving the "per query" related memory tracking to the QueryContext. QueryContext is synchronized on the per query basis, thus the locking impact should be lower. The MemoryPool class should remain only with two long fields that have to be updated under a lock (reservedBytes, reservedRevocableBytes). Updating a long under a global lock should be very lightweight operation, and thus shouldn't cause significant locking contention.
There was a problem hiding this comment.
@arhimondr : This makes sense. But will this be a lot of additional work? -- maybe we can merge this as it is (since it brings some incremental value) and leave the query level syntonization as future work? :)
There was a problem hiding this comment.
Given the high risk nature of this change and very limited improvement it brings I would rather postpone it, and do the right refactoring once we have more time.
|
This pull request has been automatically marked as stale because it has not had recent activity. If you'd still like this PR merged, please comment on the task, make sure you've addressed reviewer comments, and rebase on the latest master. Thank you for your contributions! |
Contention of lock for MemoryPool class is observed under high load.
== RELEASE NOTES ==
General Changes
*