1010import io .github .dfa1 .vortex .reader .array .DictLongArray ;
1111import io .github .dfa1 .vortex .reader .array .DoubleArray ;
1212import io .github .dfa1 .vortex .reader .array .FloatArray ;
13+ import io .github .dfa1 .vortex .reader .array .IntArray ;
1314import io .github .dfa1 .vortex .reader .array .LongArray ;
15+ import io .github .dfa1 .vortex .reader .array .ShortArray ;
1416import io .github .dfa1 .vortex .reader .array .OffsetDoubleArray ;
1517import io .github .dfa1 .vortex .reader .array .OffsetFloatArray ;
1618import io .github .dfa1 .vortex .reader .array .OffsetIntArray ;
@@ -454,6 +456,11 @@ private static long count(List<Run> runs, CodeTable table, BoolArray fVal) {
454456
455457 /// Folds the long-domain aggregate accumulator over the code-matching rows of every run.
456458 ///
459+ /// Each run-loop variant lives in its own method: one loop per compilation unit keeps every
460+ /// loop's inline decisions independent and its profile attributable per shape. (The measured
461+ /// hot-path fix was elsewhere — the fold's monomorphic aggregate read, see
462+ /// [LongAggFold#read(long)] — the split itself was performance-neutral.)
463+ ///
457464 /// @param runs the code runs in row order
458465 /// @param table the lowered match table
459466 /// @param fold the accumulator receiving each matching row
@@ -463,34 +470,71 @@ private static void scanRunsLong(List<Run> runs, CodeTable table, LongAggFold fo
463470 byte only = (byte ) table .only ();
464471 for (Run run : runs ) {
465472 MemorySegment seg = run .seg ();
466- long base = run .childBase ();
467- long rowBase = run .rowBase ();
468- long len = run .len ();
469473 if (seg != null && single ) {
470- for (long j = 0 ; j < len ; j ++) {
471- if (seg .get (ValueLayout .JAVA_BYTE , base + j ) == only ) {
472- fold .accept (rowBase + j );
473- }
474- }
474+ scanRunSingleLong (seg , run .childBase (), run .rowBase (), run .len (), only , fold );
475475 } else if (seg != null ) {
476- for (long j = 0 ; j < len ; j ++) {
477- if (match [seg .get (ValueLayout .JAVA_BYTE , base + j ) & 0xFF ]) {
478- fold .accept (rowBase + j );
479- }
480- }
476+ scanRunTableLong (seg , run .childBase (), run .rowBase (), run .len (), match , fold );
481477 } else {
482- ByteArray child = run .child ();
483- for (long j = 0 ; j < len ; j ++) {
484- if (match [child .getByte (base + j ) & 0xFF ]) {
485- fold .accept (rowBase + j );
486- }
487- }
478+ scanRunAccessorLong (run .child (), run .childBase (), run .rowBase (), run .len (), match , fold );
479+ }
480+ }
481+ }
482+
483+ /// Single-match segment scan feeding the long-domain fold.
484+ ///
485+ /// @param seg the run's backing segment
486+ /// @param base the run's first index within the segment
487+ /// @param rowBase the run's first row in the scanned view
488+ /// @param len the run length
489+ /// @param only the single matching code
490+ /// @param fold the accumulator receiving each matching row
491+ private static void scanRunSingleLong (MemorySegment seg , long base , long rowBase , long len ,
492+ byte only , LongAggFold fold ) {
493+ for (long j = 0 ; j < len ; j ++) {
494+ if (seg .get (ValueLayout .JAVA_BYTE , base + j ) == only ) {
495+ fold .accept (rowBase + j );
496+ }
497+ }
498+ }
499+
500+ /// Table-lookup segment scan feeding the long-domain fold.
501+ ///
502+ /// @param seg the run's backing segment
503+ /// @param base the run's first index within the segment
504+ /// @param rowBase the run's first row in the scanned view
505+ /// @param len the run length
506+ /// @param match the lowered per-code match table
507+ /// @param fold the accumulator receiving each matching row
508+ private static void scanRunTableLong (MemorySegment seg , long base , long rowBase , long len ,
509+ boolean [] match , LongAggFold fold ) {
510+ for (long j = 0 ; j < len ; j ++) {
511+ if (match [seg .get (ValueLayout .JAVA_BYTE , base + j ) & 0xFF ]) {
512+ fold .accept (rowBase + j );
513+ }
514+ }
515+ }
516+
517+ /// Accessor-fallback scan feeding the long-domain fold (segmentless or broadcast children).
518+ ///
519+ /// @param child the codes child owning the run
520+ /// @param base the run's first index within the child
521+ /// @param rowBase the run's first row in the scanned view
522+ /// @param len the run length
523+ /// @param match the lowered per-code match table
524+ /// @param fold the accumulator receiving each matching row
525+ private static void scanRunAccessorLong (ByteArray child , long base , long rowBase , long len ,
526+ boolean [] match , LongAggFold fold ) {
527+ for (long j = 0 ; j < len ; j ++) {
528+ if (match [child .getByte (base + j ) & 0xFF ]) {
529+ fold .accept (rowBase + j );
488530 }
489531 }
490532 }
491533
492534 /// Folds the double-domain aggregate accumulator over the code-matching rows of every run.
493535 ///
536+ /// One run-loop variant per method, mirroring [#scanRunsLong(List, CodeTable, LongAggFold)].
537+ ///
494538 /// @param runs the code runs in row order
495539 /// @param table the lowered match table
496540 /// @param fold the accumulator receiving each matching row
@@ -500,28 +544,63 @@ private static void scanRunsDouble(List<Run> runs, CodeTable table, DoubleAggFol
500544 byte only = (byte ) table .only ();
501545 for (Run run : runs ) {
502546 MemorySegment seg = run .seg ();
503- long base = run .childBase ();
504- long rowBase = run .rowBase ();
505- long len = run .len ();
506547 if (seg != null && single ) {
507- for (long j = 0 ; j < len ; j ++) {
508- if (seg .get (ValueLayout .JAVA_BYTE , base + j ) == only ) {
509- fold .accept (rowBase + j );
510- }
511- }
548+ scanRunSingleDouble (seg , run .childBase (), run .rowBase (), run .len (), only , fold );
512549 } else if (seg != null ) {
513- for (long j = 0 ; j < len ; j ++) {
514- if (match [seg .get (ValueLayout .JAVA_BYTE , base + j ) & 0xFF ]) {
515- fold .accept (rowBase + j );
516- }
517- }
550+ scanRunTableDouble (seg , run .childBase (), run .rowBase (), run .len (), match , fold );
518551 } else {
519- ByteArray child = run .child ();
520- for (long j = 0 ; j < len ; j ++) {
521- if (match [child .getByte (base + j ) & 0xFF ]) {
522- fold .accept (rowBase + j );
523- }
524- }
552+ scanRunAccessorDouble (run .child (), run .childBase (), run .rowBase (), run .len (), match , fold );
553+ }
554+ }
555+ }
556+
557+ /// Single-match segment scan feeding the double-domain fold.
558+ ///
559+ /// @param seg the run's backing segment
560+ /// @param base the run's first index within the segment
561+ /// @param rowBase the run's first row in the scanned view
562+ /// @param len the run length
563+ /// @param only the single matching code
564+ /// @param fold the accumulator receiving each matching row
565+ private static void scanRunSingleDouble (MemorySegment seg , long base , long rowBase , long len ,
566+ byte only , DoubleAggFold fold ) {
567+ for (long j = 0 ; j < len ; j ++) {
568+ if (seg .get (ValueLayout .JAVA_BYTE , base + j ) == only ) {
569+ fold .accept (rowBase + j );
570+ }
571+ }
572+ }
573+
574+ /// Table-lookup segment scan feeding the double-domain fold.
575+ ///
576+ /// @param seg the run's backing segment
577+ /// @param base the run's first index within the segment
578+ /// @param rowBase the run's first row in the scanned view
579+ /// @param len the run length
580+ /// @param match the lowered per-code match table
581+ /// @param fold the accumulator receiving each matching row
582+ private static void scanRunTableDouble (MemorySegment seg , long base , long rowBase , long len ,
583+ boolean [] match , DoubleAggFold fold ) {
584+ for (long j = 0 ; j < len ; j ++) {
585+ if (match [seg .get (ValueLayout .JAVA_BYTE , base + j ) & 0xFF ]) {
586+ fold .accept (rowBase + j );
587+ }
588+ }
589+ }
590+
591+ /// Accessor-fallback scan feeding the double-domain fold (segmentless or broadcast children).
592+ ///
593+ /// @param child the codes child owning the run
594+ /// @param base the run's first index within the child
595+ /// @param rowBase the run's first row in the scanned view
596+ /// @param len the run length
597+ /// @param match the lowered per-code match table
598+ /// @param fold the accumulator receiving each matching row
599+ private static void scanRunAccessorDouble (ByteArray child , long base , long rowBase , long len ,
600+ boolean [] match , DoubleAggFold fold ) {
601+ for (long j = 0 ; j < len ; j ++) {
602+ if (match [child .getByte (base + j ) & 0xFF ]) {
603+ fold .accept (rowBase + j );
525604 }
526605 }
527606 }
@@ -563,7 +642,7 @@ void accept(long row) {
563642 return ;
564643 }
565644 nonNull ++;
566- long v = NumericColumns . widenLong ( aggData , unsigned , row );
645+ long v = read ( row );
567646 sum += v ;
568647 if (!found ) {
569648 min = v ;
@@ -579,6 +658,31 @@ void accept(long row) {
579658 }
580659 }
581660
661+ /// Reads aggregate position `row` widened to a `long` — a deliberate private copy of
662+ /// [NumericColumns#widenLong(Array, boolean, long)]. The shared helper's receiver profile
663+ /// mixes every caller: [#matchTable(DictShape, Predicate)] feeds it the (materialized)
664+ /// dictionary pool, so the fold's hot aggregate read compiled as a bimorphic guard whose
665+ /// lost receiver type degraded the inner segment reads to virtual calls per match
666+ /// (async-profiler + PrintInlining, 2026-07-02). This copy's profile only ever sees
667+ /// aggregate receivers, keeping the read monomorphic like the sum lane's.
668+ ///
669+ /// @param row the zero-based position
670+ /// @return the widened aggregate value
671+ private long read (long row ) {
672+ Array data = aggData ;
673+ if (data instanceof LongArray la ) {
674+ return la .getLong (row );
675+ }
676+ if (data instanceof IntArray ia ) {
677+ return unsigned ? (ia .getInt (row ) & 0xFFFFFFFFL ) : ia .getInt (row );
678+ }
679+ if (data instanceof ShortArray sa ) {
680+ return unsigned ? (sa .getShort (row ) & 0xFFFFL ) : sa .getShort (row );
681+ }
682+ ByteArray ba = (ByteArray ) data ;
683+ return unsigned ? (ba .getByte (row ) & 0xFFL ) : ba .getByte (row );
684+ }
685+
582686 /// Boxes the fold exactly as the kernel's long lane does.
583687 ///
584688 /// @return the fold: selected count, non-null count, the [Long] sum, and the [Long]
@@ -626,7 +730,7 @@ void accept(long row) {
626730 return ;
627731 }
628732 nonNull ++;
629- double v = readDouble ( aggData , aggIsFloat , row );
733+ double v = read ( row );
630734 sum += v ;
631735 if (!found ) {
632736 min = v ;
@@ -642,6 +746,16 @@ void accept(long row) {
642746 }
643747 }
644748
749+ /// Reads aggregate position `row` widened to a `double` — a private copy of the shared
750+ /// helper, for the same receiver-profile isolation as the long fold's read: this call
751+ /// site's profile only ever sees aggregate receivers, keeping the read monomorphic.
752+ ///
753+ /// @param row the zero-based position
754+ /// @return the aggregate value widened to a double
755+ private double read (long row ) {
756+ return aggIsFloat ? ((FloatArray ) aggData ).getFloat (row ) : ((DoubleArray ) aggData ).getDouble (row );
757+ }
758+
645759 /// Boxes the fold exactly as the kernel's double lane does — the extremes carry the
646760 /// column's element box ([Float] for an `f32` aggregate).
647761 ///
0 commit comments