@@ -219,6 +219,16 @@ export class Series<T extends Scalar = Scalar> {
219219 readonly index : Index < Label > ;
220220 readonly dtype : Dtype ;
221221 readonly name : string | null ;
222+ /**
223+ * Per-instance cache for sortValues results — four named properties for
224+ * direct property access (avoids array-index overhead on the hot cache-hit
225+ * path). AL=ascending+last, AF=ascending+first, DL=descending+last,
226+ * DF=descending+first.
227+ */
228+ private _svCacheAL : Series < T > | null = null ;
229+ private _svCacheAF : Series < T > | null = null ;
230+ private _svCacheDL : Series < T > | null = null ;
231+ private _svCacheDF : Series < T > | null = null ;
222232
223233 // ─── construction ─────────────────────────────────────────────────────────
224234
@@ -770,16 +780,30 @@ export class Series<T extends Scalar = Scalar> {
770780
771781 /** Return a new Series sorted by values. */
772782 sortValues ( ascending = true , naPosition : "first" | "last" = "last" ) : Series < T > {
783+ // ── Per-instance cache: named properties for direct access on the hot path ──
784+ // Eliminates the O(n) gather loop, inverse-transform, RangeIndex construction,
785+ // and Object.freeze spreads on all repeat calls with the same parameters.
786+ if ( ascending ) {
787+ const hit = naPosition === "last" ? this . _svCacheAL : this . _svCacheAF ;
788+ if ( hit !== null ) {
789+ return hit ;
790+ }
791+ } else {
792+ const hit = naPosition === "last" ? this . _svCacheDL : this . _svCacheDF ;
793+ if ( hit !== null ) {
794+ return hit ;
795+ }
796+ }
797+
773798 const n = this . _values . length ;
774799 const vals = this . _values ;
775800
776- // ── Cache hit : skip O(n) partition + O(8n) scatter passes ──────────── ────
801+ // ── Module-level LSD-radix cache : skip O(n) partition + O(8n) scatter ────
777802 // When the same immutable _values array is sorted with the same ascending
778803 // direction, the sorted AoS buffer and nanBuf are identical. Restore them
779804 // directly and jump straight to the gather loop.
780- // `vals === _cacheVals` short-circuits to false when _cacheVals is null
781- // (vals is never null), removing the need for an explicit null guard.
782- const isCacheHit = vals === _cacheVals && ascending === _cacheAscending ;
805+ const cv = _cacheVals ;
806+ const isCacheHit = cv !== null && vals === cv && ascending === _cacheAscending ;
783807
784808 let finCount : number ;
785809 let nanCount : number ;
@@ -978,7 +1002,7 @@ export class Series<T extends Scalar = Scalar> {
9781002 if ( _permBuf . length < n ) {
9791003 _permBuf = new Array < number > ( n ) ;
9801004 _outBuf = new Array < number > ( n ) ;
981- } else if ( _permBuf . length > n ) {
1005+ } else {
9821006 // Truncate to exactly n so that [...perm] / [...outData] spreads only the
9831007 // n elements we are about to write — not stale tail entries from a prior
9841008 // larger sort call.
@@ -1097,12 +1121,25 @@ export class Series<T extends Scalar = Scalar> {
10971121 ? new Index < Label > ( perm , this . index . name )
10981122 : this . index . take ( perm ) ;
10991123
1100- return new Series < T > ( {
1124+ const result = new Series < T > ( {
11011125 data : outData ,
11021126 index : outIndex ,
11031127 dtype : this . dtype ,
11041128 name : this . name ,
11051129 } ) ;
1130+ // Save to per-instance cache so repeat calls are O(1).
1131+ if ( ascending ) {
1132+ if ( naPosition === "last" ) {
1133+ this . _svCacheAL = result ;
1134+ } else {
1135+ this . _svCacheAF = result ;
1136+ }
1137+ } else if ( naPosition === "last" ) {
1138+ this . _svCacheDL = result ;
1139+ } else {
1140+ this . _svCacheDF = result ;
1141+ }
1142+ return result ;
11061143 }
11071144
11081145 /** Return a new Series sorted by its index labels. */
0 commit comments