Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Removed synchronization of JyAttribute.getAttr to improve performance…
… in multithreaded scenarios.
  • Loading branch information
Stewori committed Oct 18, 2024
commit 4c271d88e6a85847b5f2d378cdbc7b3553aab2ae
75 changes: 52 additions & 23 deletions src/org/python/core/JyAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -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() {

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.

Copy link
Member

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

public static synchronized byte reserveCustomAttrType() {
if (nonBuiltinAttrTypeOffset == 0) {

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:

Copy link
Member

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.

throw new IllegalStateException("No more attr types available.");
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
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.
Copy link
Member

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:

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.

Copy link
Member Author

@Stewori Stewori Oct 19, 2024

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.

Copy link
Member

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.

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.
// 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;
}

/**
Expand Down Expand Up @@ -323,4 +352,4 @@ public static void delAttr(PyObject ob, byte attr_type) {
}
}
}
}
}
7 changes: 4 additions & 3 deletions src/org/python/core/PyObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public class PyObject implements Serializable {
* This should have been suited at {@link org.python.modules.gc}, but that would cause a
* dependency cycle in the init-phases of {@code gc.class} and {@code PyObject.class}. Now this
* boolean mirrors the presence of the {@link org.python.modules.gc#MONITOR_GLOBAL}-flag in
* Jython's gc module.<br>
* <br>
* Jython's gc module.
* <p>
* <b>Do not change manually.</b>
*/
public static boolean gcMonitorGlobal = false;
Expand All @@ -52,7 +52,8 @@ public class PyObject implements Serializable {
* @see org.python.core.JyAttribute#JAVA_PROXY_ATTR
* @see #getJavaProxy()
*/
protected Object attributes;
// See explanation in JyAttribute.getAttr for why this field has to be volatile.
protected volatile Object attributes;

/** Primitives classes their wrapper classes. */
private static final Map<Class<?>, Class<?>> primitiveMap = Generic.map();
Expand Down