-
Notifications
You must be signed in to change notification settings - Fork 212
Remove synchronization of JyAttribute.getAttr to improve performance in multithreaded scenarios. #361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It all hangs on the correctness of the argument. I find these things hard to convince myself of from just the theory.
An example of a test that arranges intense thread competition is this one I wrote to hammer the (prospective) type system. There was a risk, if I hadn't got it right, of deadlock or publication of half-finished objects.
The time measurements are made to prove the order of events rather than to measure performance. Synchronisation is explicit, so I'm not too worried about code re-ordering in my case.
The claim that the reader sees a coherent state as last written would be easier to accept if setAttr
were to make a new list, in which everything could be constructed final
, and only the head pointer would need to be volatile
(and the nonBuiltin*
counters). Assuming a setAttr
is much less common than a getAttr
, and lists are mostly short, the copy would not be expensive. You are best placed to say whether that's true.
which confirms the anticipated behavior of volatile fields for Java 5 and onwards. | ||
Write accesses in the linked attribute list are designed to update the list tail | ||
such that a concurrent read access would iterate either through the old state or | ||
the new state. No invalid intermediate state is exposed to the read access. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be important that the list node is fully initialised and stable before it can be referenced by the unsynchronised reader in code like this:
jython/src/org/python/core/JyAttribute.java
Lines 296 to 300 in 4c271d8
JyAttribute newAtt = attr_type < 0 ? | |
new AttributeLink(attr_type, value) : | |
new TransientAttributeLink(attr_type, value); | |
newAtt.setNext(att); | |
ob.attributes = newAtt; |
I see that the lines of code here are sequenced so that the publication of the reference to the new node comes last. Now we have to be sure the compiler won't rearrange assignment before construction. ISTR either the JLS or Pugh explicitly mentions this possibility. We have guarantees about writes to an individual variable, but what about actions nearby in the programme order of the thread? I think the generous application of the
volatile
keyword may have achieved that, but as I've said, I find this stuff difficult to reason about securely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand the volatile semantics, having ob.attributes
and (Transient
)AttributeLink.next
volatile should indeed prevent reordering of newAtt.setNext(att);
and ob.attributes = newAtt;
. Do you have an idea how we can confirm that? Perhaps by disassembling the bytecode of JyAttribute.class
. That would be tedious and I suspect the JIT may apply subsequent optimization, including reordering where permitted. Perhaps an isolated test of JyAttribute
concurrent access? Like 100 Threads read and write attributes permanently for 5 minutes for a dummy PyObject
. This really stresses the design and the test would observe the results to confirm the anticipated behavior. Still tedious to implement. Read access would have to confirm that concurrent write access would never make an unrelated attribute (with higher index than the written one) temporarily invisible. Provoking an exception of any kind would of course be a fast-fail for the whole test.
I'm skeptical about changing the implementation more broadly (to create an entirely new list every time). If the volatile semantics does not work as expected and promised, that approach would be similarly broken. Proper initialization of the whole new list might be reordered with assignment to ob.attribute
. A read access in the wrong moment could pick the uninitialized thing. So either it works as specified and it would be fine in any case, or it would be broken in any case. The new-list-every-time-approach would btw also require next
to be volatile to prevent reordering of list initialization. Then, if we would make a new thing every time we could also make it an array. But also then, the array would have to be volatile to ensure proper initialization. However I'm not even aware what volatile would theoretically mean for an array (every item be like a volatile field I suppose).
All in all, the question of trust in the specs applies to each approach and we would likely need a proper stress test in any case. So I would go with the already implemented solution, wich is of all alternatives the one with the fewest changes to the status quo. Also, I do not see enough benefit to justify rewriting it yet again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point about the array. And given that initialisation might skip round the write to ob.attributes
I'm not convinced it helps. The idea was to reduce the number of volatile fields so we had only one to reason about instead of 3n+1 things.
That's the sort of test I meant, but it might not have to run so long if you could guarantee the threads compete. Inspecting the output of a particular compiler is not good enough. Our correctness has to be based on good theory and a reasonably harsh practical exam.
Before we think about writing a stress test I suggest to await response of @newprogrammer101 . If the PR does not solve the issue or blows something up, we would need to rethink the solution anyway. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (nonBuiltinAttrTypeOffset == 0) { throw new IllegalStateException("No more attr types available."); }
Magic numbers are being used in conditional checks.
Recommendation: Define these values as constants to improve code readability and maintainability
public static byte reserveCustomAttrType() {
public static synchronized byte reserveCustomAttrType() {
The method reserveCustomAttrType has been changed to a synchronised method. This is a good practice for thread safety, but it introduces a potential bottleneck under heavy load.
Recommendation: Assess if finer-grained synchronisation (e.g., using atomic variables) could replace the synchronisation here to improve concurrency without sacrificing thread safety.
|
||
/** | ||
* Reserves and returns a new non-transient attr type for custom use. | ||
* | ||
* @return a non-transient attr type for custom use | ||
*/ | ||
public static byte reserveCustomAttrType() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observation: The method reserveCustomAttrType has been changed to a synchronised method. This is a good practice for thread safety, but it introduces a potential bottleneck under heavy load.
Recommendation: Assess if finer-grained synchronisation (e.g., using atomic variables) could replace the synchronisation here to improve concurrency without sacrificing thread safety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AI? Infrequently called, and relieves bottlenecks elsewhere. (Point of PR.)
|
||
/** | ||
* Reserves and returns a new non-transient attr type for custom use. | ||
* | ||
* @return a non-transient attr type for custom use | ||
*/ | ||
public static byte reserveCustomAttrType() { | ||
public static synchronized byte reserveCustomAttrType() { | ||
if (nonBuiltinAttrTypeOffset == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Magic numbers are being used in conditional checks.
Recommended : Define these values as constants to improve code readability and maintainability:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be valid, but not in scope of change.
Thanks @Stewori for the quick change in this CR. This commit is to resolve concurrency issue on Jython package. But currently when I created multi JythonExecutor instance for testing purpose, I found CPU was at BLOCK state on |
Fixes #360