2626import com .google .common .annotations .VisibleForTesting ;
2727import com .google .common .base .Strings ;
2828import com .google .common .base .Verify ;
29+ import com .google .common .collect .HashMultiset ;
2930import com .google .common .collect .ImmutableList ;
3031import com .google .common .collect .ImmutableListMultimap ;
3132import com .google .common .collect .ImmutableMap ;
3233import com .google .common .collect .ImmutableSet ;
3334import com .google .common .collect .Multimaps ;
35+ import com .google .common .collect .Multiset ;
3436import com .google .common .collect .Sets ;
3537import com .google .common .collect .Streams ;
3638import com .google .errorprone .annotations .CanIgnoreReturnValue ;
@@ -414,15 +416,40 @@ private ImmutableList<CelMutableExpr> getCseCandidates(
414416 navAst .getRoot ().allNodes (TraversalOrder .PRE_ORDER ).collect (toImmutableList ());
415417 ImmutableListMultimap <Integer , CelNavigableMutableExpr > nodesByHeight =
416418 Multimaps .index (allNodes , CelNavigableMutableExpr ::height );
419+ // Loop-invariant subexpressions are extracted even if they occur once: block slots are lazily
420+ // memoized, so they then run at most once and only if reached, instead of once per iteration.
421+ @ SuppressWarnings ("ValueClassIdentity" ) // Integer values are only unboxed, never compared.
422+ IdentityHashMap <CelMutableExpr , Integer > loopDepths = new IdentityHashMap <>();
423+ for (CelNavigableMutableExpr node : allNodes ) {
424+ if (node .getKind ().equals (Kind .COMPREHENSION )) {
425+ CelNavigableMutableExpr .fromExpr (node .expr ().comprehension ().loopStep ())
426+ .allNodes ()
427+ .forEach (step -> loopDepths .merge (step .expr (), 1 , Integer ::sum ));
428+ }
429+ }
430+ // Only hoisting needs these, and computing them walks every node's subtree.
431+ Set <CelMutableExpr > eligibleExprs = Sets .newIdentityHashSet ();
432+ Multiset <Integer > vnCounts = HashMultiset .create ();
433+ if (!loopDepths .isEmpty ()) {
434+ for (CelNavigableMutableExpr node : allNodes ) {
435+ if (canEliminate (node , ineligibleExprs )) {
436+ eligibleExprs .add (node .expr ());
437+ vnCounts .add (valueNumberer .vnOf (node .expr ()));
438+ }
439+ }
440+ }
417441 Set <Integer > seenVns = new HashSet <>();
418442 for (CelNavigableMutableExpr node : allNodes ) {
419- ImmutableList <CelNavigableMutableExpr > sameHeightNodes = nodesByHeight .get (node .height ());
420- if (sameHeightNodes .size () < 2 || !canEliminate (node , ineligibleExprs )) {
443+ boolean inLoopStep = loopDepths .containsKey (node .expr ());
444+ if ((!inLoopStep && nodesByHeight .get (node .height ()).size () < 2 )
445+ || !canEliminate (node , ineligibleExprs )) {
421446 continue ;
422447 }
423- int vn = valueNumberer .vnOf (node .expr ());
424- if (!seenVns .add (vn )) {
425- return sameHeightNodes .stream ()
448+ CelNavigableMutableExpr target =
449+ inLoopStep ? hoistTarget (node , loopDepths , vnCounts , valueNumberer , eligibleExprs ) : node ;
450+ int vn = valueNumberer .vnOf (target .expr ());
451+ if (!seenVns .add (vn ) || inLoopStep ) {
452+ return nodesByHeight .get (target .height ()).stream ()
426453 .filter (candidate -> canEliminate (candidate , ineligibleExprs ))
427454 .map (CelNavigableMutableExpr ::expr )
428455 .filter (expr -> valueNumberer .vnOf (expr ) == vn )
@@ -433,6 +460,41 @@ private ImmutableList<CelMutableExpr> getCseCandidates(
433460 return ImmutableList .of ();
434461 }
435462
463+ // Slots are never revisited, so before hoisting `node` this first hoists any eligible descendant
464+ // that also occurs outside of it, or that is invariant to a loop nested inside it.
465+ private static CelNavigableMutableExpr hoistTarget (
466+ CelNavigableMutableExpr node ,
467+ IdentityHashMap <CelMutableExpr , Integer > loopDepths ,
468+ Multiset <Integer > vnCounts ,
469+ ValueNumberer valueNumberer ,
470+ Set <CelMutableExpr > eligibleExprs ) {
471+ // A descendant occurring more often than `node` is duplicated within it or shared outside it.
472+ int copies = vnCounts .count (valueNumberer .vnOf (node .expr ()));
473+ int depth = loopDepths .get (node .expr ());
474+ return node .descendants (TraversalOrder .PRE_ORDER )
475+ .filter (
476+ d ->
477+ eligibleExprs .contains (d .expr ())
478+ && (vnCounts .count (valueNumberer .vnOf (d .expr ())) > copies
479+ || loopDepths .get (d .expr ()) > depth ))
480+ .findFirst ()
481+ .map (d -> hoistTarget (d , loopDepths , vnCounts , valueNumberer , eligibleExprs ))
482+ .orElse (node );
483+ }
484+
485+ // The planner accumulates `@ac + [elem]` steps (map, filter) in place, so `[elem]` is not
486+ // extracted.
487+ private static boolean isAccumulation (CelMutableExpr expr ) {
488+ if (!expr .getKind ().equals (Kind .CALL )
489+ || !expr .call ().function ().equals (Operator .ADD .getFunction ())) {
490+ return false ;
491+ }
492+ CelMutableExpr accu = expr .call ().args ().get (0 );
493+ return accu .getKind ().equals (Kind .IDENT )
494+ && accu .ident ().name ().startsWith (MANGLED_COMPREHENSION_ACCU_VAR_PREFIX )
495+ && expr .call ().args ().get (1 ).getKind ().equals (Kind .LIST );
496+ }
497+
436498 private boolean canEliminate (
437499 CelNavigableMutableExpr navigableExpr , Set <CelMutableExpr > ineligibleExprs ) {
438500 return !navigableExpr .getKind ().equals (Kind .CONSTANT )
@@ -488,6 +550,12 @@ private static Set<CelMutableExpr> getIneligibleExprsFromComprehensionBranches(
488550 .allNodes ())
489551 .map (CelNavigableMutableExpr ::expr )
490552 .forEach (ineligibleExprs ::add ));
553+ navAst
554+ .getRoot ()
555+ .allNodes ()
556+ .map (CelNavigableMutableExpr ::expr )
557+ .filter (SubexpressionOptimizer ::isAccumulation )
558+ .forEach (expr -> ineligibleExprs .add (expr .call ().args ().get (1 )));
491559
492560 return ineligibleExprs ;
493561 }
0 commit comments