From 8cb71e9f051a8a0b567a6ab1607ee40704053867 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 19 Sep 2026 10:05:54 +0200 Subject: [PATCH 1/2] perf: avoid discarded simple list assignment results Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- .../perlonjava/backend/jvm/EmitVariable.java | 14 ++- .../runtime/runtimetypes/RuntimeBase.java | 9 ++ .../runtime/runtimetypes/RuntimeList.java | 106 ++++++++++-------- .../resources/unit/void_list_assignment.t | 24 ++++ 4 files changed, 103 insertions(+), 50 deletions(-) create mode 100644 src/test/resources/unit/void_list_assignment.t diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index ff44739683..d68d8a1d4e 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -1117,7 +1117,17 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo // The my operator needs to be processed to create the variables first. node.left.accept(emitterVisitor.with(RuntimeContextType.LVALUE_LIST)); // emit the variable (target) mv.visitVarInsn(Opcodes.ALOAD, rhsListSlot); // reload RHS list - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "setFromList", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;", false); + boolean discardResult = emitterVisitor.ctx.contextType == RuntimeContextType.VOID; + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/runtimetypes/RuntimeBase", + discardResult ? "setFromListVoid" : "setFromList", + discardResult ? "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)V" + : "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;", + false); + // The shared void-context epilogue consumes one stack value. + // Keep its existing stack contract without materializing a + // RuntimeArray assignment result. + if (discardResult) mv.visitInsn(Opcodes.ACONST_NULL); if (pooledRhsList) { ctx.javaClassInfo.releaseSpillSlot(); @@ -1127,7 +1137,7 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo // caller's context. RuntimeArray.scalar() uses the RHS // element count recorded by setFromList(). emitRuntimeContextConversion(emitterVisitor, "@"); - } else { + } else if (!discardResult) { EmitOperator.handleScalarContext(emitterVisitor, node); } break; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java index 632922405a..789a2eef00 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeBase.java @@ -917,6 +917,15 @@ public RuntimeScalar createReferenceWithTrackedElements() { */ public abstract RuntimeArray setFromList(RuntimeList list); + /** + * Sets this value from a list when the Perl assignment result is discarded. + * Subclasses may avoid materializing that result while retaining assignment + * side effects. The default preserves ordinary assignment semantics. + */ + public void setFromListVoid(RuntimeList list) { + setFromList(list); + } + /** * Retrieves the result of keys() as a RuntimeArray instance. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index 7e48ec0242..f7a0766ef9 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -582,54 +582,8 @@ public RuntimeList createListReference() { public RuntimeArray setFromList(RuntimeList value) { // Fast path: LHS is all simple scalars, RHS is a single RuntimeArray // This handles the common case: my ($a, $b) = @_ - if (value.elements.size() == 1 && value.elements.get(0) instanceof RuntimeArray rhsArray) { - boolean allSimpleScalars = true; - for (RuntimeBase elem : elements) { - if (!(elem instanceof RuntimeScalar) || elem instanceof RuntimeScalarReadOnly) { - allSimpleScalars = false; - break; - } - } - if (allSimpleScalars) { - // Suppress MortalList.flush() during LHS assignments, matching - // the slow path below. Without this, a blessed return value - // (e.g., Holler->new()) passed as an argument following a - // reference-typed arg can fire DESTROY mid-assignment when - // an earlier lhs.set() triggers setLargeRefCounted → flush() - // before the blessed value's own lhs.set() captures it. - // Repros: t/tt_leak.t tests 5, 9 (TT stash updates with - // blessed temps as values). - boolean wasFlushing = MortalList.suppressFlush(true); - try { - List rhsElements = rhsArray.elements; - int rhsSize = rhsElements.size(); - int lhsSize = elements.size(); - - // Copy RHS values first to handle aliasing (e.g., ($a,$b) = ($b,$a)) - RuntimeScalar[] rhsValues = new RuntimeScalar[Math.min(lhsSize, rhsSize)]; - for (int i = 0; i < rhsValues.length; i++) { - RuntimeScalar elem = rhsElements.get(i); - // Handle null elements (from delete $array[i]) - rhsValues[i] = (elem == null) ? new RuntimeScalar() : new RuntimeScalar(elem); - } - - RuntimeArray result = new RuntimeArray(lhsSize); - result.scalarContextSize = rhsSize; - for (int i = 0; i < lhsSize; i++) { - RuntimeScalar lhs = (RuntimeScalar) elements.get(i); - if (i < rhsValues.length) { - lhs.set(rhsValues[i]); - } else { - lhs.set(new RuntimeScalar()); - } - result.elements.add(lhs); - } - return result; - } finally { - MortalList.suppressFlush(wasFlushing); - } - } - } + RuntimeArray simpleRhs = simpleScalarListAssignmentRhs(value); + if (simpleRhs != null) return assignSimpleScalarList(simpleRhs, true); boolean hasUndefPlaceholderLhs = false; for (RuntimeBase elem : elements) { @@ -803,6 +757,62 @@ public RuntimeArray setFromList(RuntimeList value) { return result; } + @Override + public void setFromListVoid(RuntimeList value) { + RuntimeArray simpleRhs = simpleScalarListAssignmentRhs(value); + if (simpleRhs != null) { + assignSimpleScalarList(simpleRhs, false); + return; + } + setFromList(value); + } + + private RuntimeArray simpleScalarListAssignmentRhs(RuntimeList value) { + if (value.elements.size() != 1 || !(value.elements.get(0) instanceof RuntimeArray rhsArray)) { + return null; + } + for (RuntimeBase elem : elements) { + if (!(elem instanceof RuntimeScalar) || elem instanceof RuntimeScalarReadOnly) { + return null; + } + } + return rhsArray; + } + + private RuntimeArray assignSimpleScalarList(RuntimeArray rhsArray, boolean materializeResult) { + // Suppress MortalList.flush() during LHS assignments, matching the slow + // path below. A blessed return value passed beside a reference-typed + // argument must remain alive until every LHS slot captures it. + boolean wasFlushing = MortalList.suppressFlush(true); + try { + List rhsElements = rhsArray.elements; + int rhsSize = rhsElements.size(); + int lhsSize = elements.size(); + + // Copy RHS values first to handle aliasing (e.g., ($a,$b) = ($b,$a)). + RuntimeScalar[] rhsValues = new RuntimeScalar[Math.min(lhsSize, rhsSize)]; + for (int i = 0; i < rhsValues.length; i++) { + RuntimeScalar elem = rhsElements.get(i); + rhsValues[i] = (elem == null) ? new RuntimeScalar() : new RuntimeScalar(elem); + } + + RuntimeArray result = materializeResult ? new RuntimeArray(lhsSize) : null; + if (result != null) result.scalarContextSize = rhsSize; + for (int i = 0; i < lhsSize; i++) { + RuntimeScalar lhs = (RuntimeScalar) elements.get(i); + if (i < rhsValues.length) { + lhs.set(rhsValues[i]); + } else { + lhs.set(new RuntimeScalar()); + } + if (result != null) result.elements.add(lhs); + } + return result; + } finally { + MortalList.suppressFlush(wasFlushing); + } + } + /** * Converts the list to a string, concatenating all elements without separators. * diff --git a/src/test/resources/unit/void_list_assignment.t b/src/test/resources/unit/void_list_assignment.t new file mode 100644 index 0000000000..e88c60543f --- /dev/null +++ b/src/test/resources/unit/void_list_assignment.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More; + +sub bind_and_format { + my ($left, $right, $missing) = @_; + return join ':', map { defined $_ ? $_ : 'undef' } $left, $right, $missing; +} + +is bind_and_format('alpha', 'beta'), 'alpha:beta:undef', + 'void-context parameter binding preserves supplied and missing arguments'; + +my ($first, $second) = ('left', 'right'); +($first, $second) = ($second, $first); +is_deeply [$first, $second], ['right', 'left'], + 'void list assignment preserves RHS values before aliased LHS writes'; + +my ($one, $two); +my $count = scalar(($one, $two) = qw(one two)); +is $count, 2, 'non-void list assignment still returns its scalar element count'; +is_deeply [$one, $two], [qw(one two)], + 'non-void list assignment still assigns every element'; + +done_testing; From 65332c259bdb66c78ae8f1a37f07a95219dade52 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 19 Sep 2026 11:06:56 +0200 Subject: [PATCH 2/2] perf: avoid discarded simple list assignment results Avoid constructing a RuntimeArray result when a simple scalar list assignment is evaluated in void context, while retaining the observable result path. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- docs/about/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 6fcf1a2877..3e54884963 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -29,6 +29,9 @@ priorities and future plans. - Avoid arbitrary-precision allocation for ordinary native-integer unsigned shifts while retaining the existing wide-integer fallback. +- Avoid materializing discarded simple list-assignment values for ordinary + scalar argument binding while retaining the observable result path. + - Avoid redundant active-runtime lookups while binding and restoring implicit global `$_` `foreach` aliases.