Skip to content

Commit 8d585c6

Browse files
committed
op.c: Further optimisations of foreach+indexed to apply to LIST as well
This builds on the work of commit a8394b4, expanding it to apply to any `foreach` loop iterating over any kind of list, not just an in-place array.
1 parent d126053 commit 8d585c6

File tree

6 files changed

+118
-34
lines changed

6 files changed

+118
-34
lines changed

lib/B/Deparse.t

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,6 +2615,10 @@ foreach my ($idx, $elem) (builtin::indexed @arr) {
26152615
die;
26162616
}
26172617
####
2618+
foreach my ($idx, $elem) (builtin::indexed 'x', 'y', 'z') {
2619+
die;
2620+
}
2621+
####
26182622
my @ducks;
26192623
foreach my ($tick, $trick, $track) (@ducks) {
26202624
study $_;

lib/builtin.t

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ package FetchStoreCounter {
357357
}
358358

359359
ok(eq_array(\@output, [qw( [0]=zero [1]=one [2]=two [3]=three [4]=four [5]=five )] ),
360-
'foreach + builtin::indexed' );
360+
'foreach + builtin::indexed ARRAY' );
361361

362362
undef @output;
363363

@@ -368,7 +368,16 @@ package FetchStoreCounter {
368368
}
369369

370370
ok(eq_array(\@output, [qw( [0]=zero [1]=one [2]=two [3]=three [4]=four [5]=five )] ),
371-
'foreach + imported indexed' );
371+
'foreach + imported indexed ARRAY' );
372+
373+
undef @output;
374+
375+
foreach my ( $idx, $val ) ( builtin::indexed qw( six seven eight nine ) ) {
376+
push @output, "[$idx]=$val";
377+
}
378+
379+
ok(eq_array(\@output, [qw( [0]=six [1]=seven [2]=eight [3]=nine )] ),
380+
'foreach + builtin::indexed LIST' );
372381
}
373382

374383
# Vanilla trim tests

op.c

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9672,6 +9672,17 @@ S_op_is_cv_xsub(pTHX_ OP *o, XSUBADDR_t xsub)
96729672
return CvXSUB(cv) == xsub;
96739673
}
96749674

9675+
#define op_is_call_to_cv_xsub(o, xsub) S_op_is_call_to_cv_xsub(aTHX_ o, xsub)
9676+
static bool
9677+
S_op_is_call_to_cv_xsub(pTHX_ OP *o, XSUBADDR_t xsub)
9678+
{
9679+
if(o->op_type != OP_ENTERSUB)
9680+
return false;
9681+
9682+
OP *cvop = cLISTOPx(cUNOPo->op_first)->op_last;
9683+
return op_is_cv_xsub(cvop, xsub);
9684+
}
9685+
96759686
/*
96769687
=for apidoc newFOROP
96779688

@@ -9812,45 +9823,64 @@ Perl_newFOROP(pTHX_ I32 flags, OP *sv, OP *expr, OP *block, OP *cont)
98129823
enteriterpflags |= OPpITER_DEF;
98139824
}
98149825

9815-
if (expr->op_type == OP_RV2AV || expr->op_type == OP_PADAV) {
9816-
expr = op_lvalue(op_force_list(scalar(ref(expr, OP_ITER))), OP_GREPSTART);
9817-
enteriterflags |= OPf_STACKED;
9818-
}
9819-
else if (padoff != 0 && how_many_more == 1 && /* two lexical vars */
9820-
expr->op_type == OP_ENTERSUB) {
9826+
if (padoff != 0 && how_many_more == 1 && /* two lexical vars */
9827+
op_is_call_to_cv_xsub(expr, &Perl_XS_builtin_indexed)) { /* expr is a call to builtin::indexed */
9828+
/* Turn the OP_ENTERSUB into a regular OP_LIST without the final CV,
9829+
* and set the OPpITER_INDEXED flag instead */
98219830
OP *args = cUNOPx(expr)->op_first;
98229831
assert(OP_TYPE_IS_OR_WAS(args, OP_LIST));
98239832

9824-
OP *pre_firstarg = NULL;
9825-
OP *firstarg = cLISTOPx(args)->op_first;
9826-
OP *lastarg = cLISTOPx(args)->op_last;
9833+
OP *first = cLISTOPx(args)->op_first;
9834+
/* OP_PUSHMARK must remain */
9835+
assert(first->op_type == OP_PUSHMARK);
9836+
first = OpSIBLING(first);
98279837

9828-
if(firstarg->op_type == OP_PUSHMARK)
9829-
pre_firstarg = firstarg, firstarg = OpSIBLING(firstarg);
9830-
if(firstarg == lastarg)
9831-
firstarg = NULL;
9838+
OP *pre_last = NULL, *last = first;
9839+
while(OpHAS_SIBLING(last))
9840+
pre_last = last, last = OpSIBLING(last);
9841+
if(pre_last) {
9842+
/* splice out the final CV op */
9843+
cLISTOPx(args)->op_last = pre_last;
9844+
OpLASTSIB_set(pre_last, args);
98329845

9833-
if (op_is_cv_xsub(lastarg, &Perl_XS_builtin_indexed) && /* a call to builtin::indexed */
9834-
firstarg && OpSIBLING(firstarg) == lastarg && /* with one arg */
9835-
(firstarg->op_type == OP_RV2AV || firstarg->op_type == OP_PADAV) /* ... which is an array */
9836-
) {
9837-
/* Turn for my ($idx, $val) (indexed @arr) into a similar OPf_STACKED
9838-
* loop on the array itself as the case above, plus a flag to tell
9839-
* pp_iter to set the index directly
9840-
*/
9846+
op_free(last);
98419847

9842-
/* Cut the array arg out of the args list and discard the rest of
9843-
* the original expr
9844-
*/
9845-
op_sibling_splice(args, pre_firstarg, 1, NULL);
9848+
last = pre_last;
9849+
}
9850+
9851+
if(first == last && (first->op_type == OP_PADAV || first->op_type == OP_RV2AV)) {
9852+
/* Preserve the ARRAY shortcut */
9853+
OpLASTSIB_set(cLISTOPx(args)->op_first, args);
98469854
op_free(expr);
98479855

9848-
expr = op_lvalue(op_force_list(scalar(ref(firstarg, OP_ITER))), OP_GREPSTART);
9849-
enteriterflags |= OPf_STACKED;
9850-
iterpflags |= OPpITER_INDEXED;
9856+
OpLASTSIB_set(first, NULL);
9857+
expr = first;
98519858
}
9852-
else
9853-
goto expr_not_special;
9859+
else {
9860+
/* the op_targ slot contained the "was" op_type for an
9861+
* OP_NULL; clear it or op_free() will get very confused */
9862+
args->op_targ = 0;
9863+
OpTYPE_set(args, OP_LIST);
9864+
OpLASTSIB_set(args, NULL);
9865+
9866+
expr->op_flags &= ~OPf_KIDS;
9867+
cUNOPx(expr)->op_first = NULL;
9868+
op_free(expr);
9869+
9870+
expr = args;
9871+
}
9872+
9873+
/* expr's parent has currently been set to NULL, but that's OK. When
9874+
* it gets consumed by the LOOP* structure later to make the loop op
9875+
* itself this will get set correctly.
9876+
*/
9877+
9878+
iterpflags |= OPpITER_INDEXED;
9879+
}
9880+
9881+
if (expr->op_type == OP_RV2AV || expr->op_type == OP_PADAV) {
9882+
expr = op_lvalue(op_force_list(scalar(ref(expr, OP_ITER))), OP_GREPSTART);
9883+
enteriterflags |= OPf_STACKED;
98549884
}
98559885
else if (expr->op_type == OP_NULL &&
98569886
(expr->op_flags & OPf_KIDS) &&
@@ -9882,7 +9912,6 @@ Perl_newFOROP(pTHX_ I32 flags, OP *sv, OP *expr, OP *block, OP *cont)
98829912
enteriterflags |= OPf_STACKED;
98839913
}
98849914
else {
9885-
expr_not_special:
98869915
expr = op_lvalue(op_force_list(expr), OP_GREPSTART);
98879916
}
98889917

pod/perldelta.pod

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,30 @@ There may well be none in a stable release.
105105

106106
XXX
107107

108+
=item *
109+
110+
Code that uses the C<indexed> function from the L<builtin> module to generate
111+
a list of index/value pairs out of an array or list which is then passed into
112+
a two-variable C<foreach> list to unpack those again is now optimised to be
113+
more efficient.
114+
115+
my @array = (...);
116+
117+
foreach my ($idx, $val) (builtin::indexed @array) {
118+
...
119+
}
120+
121+
Z<>
122+
123+
foreach my ($idx, $val) (builtin::indexed LIST...) {
124+
...
125+
}
126+
127+
In particular, a temporary list twice the size of the original is no longer
128+
generated. Instead, the loop iterates down the original array or list
129+
in-place directly, in the same way that C<foreach (@array)> or
130+
C<foreach (LIST)> would do.
131+
108132
=back
109133

110134
=head1 Modules and Pragmata

pp_hot.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5031,6 +5031,16 @@ PP(pp_iter)
50315031
sv = PL_stack_base[ix];
50325032
}
50335033

5034+
if (UNLIKELY(pflags & OPpITER_INDEXED) && (i == 0)) {
5035+
SvREFCNT_dec(*itersvp);
5036+
/* here ix is really a stack pointer offset; we have to
5037+
* calculate the real index */
5038+
*itersvp = newSViv(ix - cx->blk_loop.state_u.stack.basesp - 1);
5039+
5040+
++i;
5041+
++itersvp;
5042+
}
5043+
50345044
av = NULL;
50355045
goto loop_ary_common;
50365046

t/perf/opcount.t

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ test_opcount(0, "Empty anonhash ref and direct lexical assignment",
10111011
srefgen => 1,
10121012
});
10131013

1014-
test_opcount(0, "foreach 2 lexicals on builtin::indexed",
1014+
test_opcount(0, "foreach 2 lexicals on builtin::indexed ARRAY",
10151015
sub { my @input = (); foreach my ($i, $x) (builtin::indexed @input) { } },
10161016
{
10171017
entersub => 0, # no call to builtin::indexed
@@ -1020,4 +1020,12 @@ test_opcount(0, "foreach 2 lexicals on builtin::indexed",
10201020
padav => 2,
10211021
});
10221022

1023+
test_opcount(0, "foreach 2 lexicals on builtin::indexed LIST",
1024+
sub { foreach my ($i, $x) (builtin::indexed qw( x y z )) { } },
1025+
{
1026+
entersub => 0, # no call to builtin::indexed
1027+
enteriter => 1,
1028+
iter => 1,
1029+
});
1030+
10231031
done_testing();

0 commit comments

Comments
 (0)