-
Notifications
You must be signed in to change notification settings - Fork 226
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?
Remove synchronization of JyAttribute.getAttr to improve performance in multithreaded scenarios. #361
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
… in multithreaded scenarios.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,6 @@ | |||||||||||
| import java.io.Serializable; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * <p> | ||||||||||||
| * Manages a linked list of general purpose Object-attributes that | ||||||||||||
| * can be attached to arbitrary {@link org.python.core.PyObject}s. | ||||||||||||
| * This method replaces the formerly used method of maintaining weak | ||||||||||||
|
|
@@ -13,20 +12,17 @@ | |||||||||||
| * to attach | ||||||||||||
| * {@link org.python.modules._weakref.GlobalRef}-objects in the | ||||||||||||
| * {@link org.python.modules._weakref.WeakrefModule}. | ||||||||||||
| * </p> | ||||||||||||
| * <p> | ||||||||||||
| * Attributes attached via the weak hash-map-method break, if the | ||||||||||||
| * {@code PyObject} is resurrected in its finalizer. The | ||||||||||||
| * {@code JyAttribute}-method is resurrection-safe. | ||||||||||||
| * </p> | ||||||||||||
| * <p> | ||||||||||||
| * To reduce memory footprint of {@code PyObject}s, the fields for | ||||||||||||
| * {@link org.python.core.finalization.FinalizeTrigger}s and | ||||||||||||
| * {@code javaProxy} are included in the list; {@code javaProxy} always | ||||||||||||
| * on top so there is no speed-regression, | ||||||||||||
| * {@link org.python.core.finalization.FinalizeTrigger} on bottom, | ||||||||||||
| * as it is usually never accessed. | ||||||||||||
| * </p> | ||||||||||||
| */ | ||||||||||||
| public abstract class JyAttribute implements Serializable { | ||||||||||||
| /* ordered by priority; indices >= 0 indicate transient attributes. | ||||||||||||
|
|
@@ -48,7 +44,7 @@ public abstract class JyAttribute implements Serializable { | |||||||||||
| public static final byte WEAK_REF_ATTR = 0; //first transient | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Reserved for use by <a href="http://www.jyni.org" target="_blank">JyNI</a>. | ||||||||||||
| * Reserved for use by <a href="https://jyni.12hp.de" target="_blank">JyNI</a>. | ||||||||||||
| */ | ||||||||||||
| public static final byte JYNI_HANDLE_ATTR = 1; | ||||||||||||
|
|
||||||||||||
|
|
@@ -87,15 +83,15 @@ public abstract class JyAttribute implements Serializable { | |||||||||||
| public static final byte GC_DELAYED_FINALIZE_CRITICAL_MARK_ATTR = 6; | ||||||||||||
|
|
||||||||||||
| public static final byte FINALIZE_TRIGGER_ATTR = Byte.MAX_VALUE; | ||||||||||||
| private static byte nonBuiltinAttrTypeOffset = Byte.MIN_VALUE+1; | ||||||||||||
| private static byte nonBuiltinTransientAttrTypeOffset = 7; | ||||||||||||
| private static volatile byte nonBuiltinAttrTypeOffset = Byte.MIN_VALUE+1; | ||||||||||||
| private static volatile byte nonBuiltinTransientAttrTypeOffset = 7; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * 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 commentThe reason will be displayed to describe this comment to others. Learn more. Magic numbers are being used in conditional checks.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May be valid, but not in scope of change. |
||||||||||||
| throw new IllegalStateException("No more attr types available."); | ||||||||||||
| } | ||||||||||||
|
|
@@ -107,18 +103,18 @@ public static byte reserveCustomAttrType() { | |||||||||||
| * | ||||||||||||
| * @return a transient attr type for custom use | ||||||||||||
| */ | ||||||||||||
| public static byte reserveTransientCustomAttrType() { | ||||||||||||
| public static synchronized byte reserveTransientCustomAttrType() { | ||||||||||||
| if (nonBuiltinTransientAttrTypeOffset == Byte.MAX_VALUE) { | ||||||||||||
| throw new IllegalStateException("No more transient attr types available."); | ||||||||||||
| } | ||||||||||||
| return nonBuiltinTransientAttrTypeOffset++; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| byte attr_type; | ||||||||||||
| final byte attr_type; | ||||||||||||
|
|
||||||||||||
| static class AttributeLink extends JyAttribute { | ||||||||||||
| JyAttribute next; | ||||||||||||
| Object value; | ||||||||||||
| volatile JyAttribute next; | ||||||||||||
| volatile Object value; | ||||||||||||
|
|
||||||||||||
| protected AttributeLink(byte attr_type, Object value) { | ||||||||||||
| super(attr_type); | ||||||||||||
|
|
@@ -147,8 +143,8 @@ protected void setValue(Object value) { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| static class TransientAttributeLink extends JyAttribute { | ||||||||||||
| transient JyAttribute next; | ||||||||||||
| transient Object value; | ||||||||||||
| volatile transient JyAttribute next; | ||||||||||||
| volatile transient Object value; | ||||||||||||
|
|
||||||||||||
| protected TransientAttributeLink(byte attr_type, Object value) { | ||||||||||||
| super(attr_type); | ||||||||||||
|
|
@@ -197,18 +193,51 @@ public static boolean hasAttr(PyObject ob, byte attr_type) { | |||||||||||
| * Retrieves the attribute of the given type from the given | ||||||||||||
| * {@link org.python.core.PyObject}. | ||||||||||||
| * If no attribute of the given type is attached, null is returned. | ||||||||||||
| * | ||||||||||||
| * This class is generally thread safe, but this method is not synchronized with | ||||||||||||
| * concurrent write access to avoid performance issues for multithreaded scenarios. | ||||||||||||
| * The trade-off for that choice is that in case of concurrent write access, it is | ||||||||||||
| * unspecified whether this method returns according to the old state or the new state. | ||||||||||||
| * This method is thread safe in the sense that a concurrent write access never exposes | ||||||||||||
| * an invalid intermediate state to this read access. | ||||||||||||
| */ | ||||||||||||
| public static Object getAttr(PyObject ob, byte attr_type) { | ||||||||||||
| synchronized (ob) { | ||||||||||||
| if (ob.attributes instanceof JyAttribute) { | ||||||||||||
| JyAttribute att = (JyAttribute) ob.attributes; | ||||||||||||
| while (att != null && att.attr_type < attr_type) { | ||||||||||||
| att = att.getNext(); | ||||||||||||
| } | ||||||||||||
| return att != null && att.attr_type == attr_type ? att.getValue() : null; | ||||||||||||
| /* | ||||||||||||
| Here used to be a synchronized(ob) block encapsulating the whole method code. | ||||||||||||
| It has been removed due to performance issues reported in | ||||||||||||
| github.com/jython/jython/issues/360. | ||||||||||||
| By making ob.attributes in PyObject and next and value in AttributeLink and | ||||||||||||
| TransientAttributeLink volatile, the read access in this method does not | ||||||||||||
| fatally interfere with a concurrent write operation. | ||||||||||||
| (Write operations continue to be synchronized against each other.) | ||||||||||||
| https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html#jls-17.4.4 states: | ||||||||||||
| "A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads | ||||||||||||
| of v by any thread" | ||||||||||||
| This induces an x happens-before y relation hb(x, y) according to | ||||||||||||
| https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html#jls-17.4.5: | ||||||||||||
| "If an action x synchronizes-with a following action y, then we also have hb(x, y)." | ||||||||||||
| Also see https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html, | ||||||||||||
| 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 | ||||||||||||
jeff5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| 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. | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand the volatile semantics, having 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 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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. |
||||||||||||
| Due to volatile semantics, this assumption cannot be broken by code reordering | ||||||||||||
| issues like pointed out in the linked DoubleCheckedLocking document. | ||||||||||||
| */ | ||||||||||||
| Object oatt = ob.attributes; // atomic one-time access to ob.attributes, which is volatile | ||||||||||||
| if (oatt instanceof JyAttribute) { | ||||||||||||
| JyAttribute att = (JyAttribute) oatt; | ||||||||||||
| while (att != null && att.attr_type < attr_type) { | ||||||||||||
| att = att.getNext(); // atomic access to next, which is volatile | ||||||||||||
| // Perhaps att is affected by a concurrent write access, perhaps not. | ||||||||||||
jeff5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| // Since the next-field is volatile, att is always a properly initialized | ||||||||||||
| // attribute sequence head at this point. | ||||||||||||
| // The overall iteration is therefore either according to the old state | ||||||||||||
| // or according to the new state. | ||||||||||||
| } | ||||||||||||
| return attr_type == JAVA_PROXY_ATTR ? ob.attributes : null; | ||||||||||||
| return att != null && att.attr_type == attr_type ? att.getValue() : null; | ||||||||||||
| } | ||||||||||||
| return attr_type == JAVA_PROXY_ATTR ? oatt : null; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
@@ -323,4 +352,4 @@ public static void delAttr(PyObject ob, byte attr_type) { | |||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
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.)