From 85d9e3fde97360ca21a19b16b304e57d24aee5dc Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 24 Oct 2026 23:12:07 +0200 Subject: [PATCH] perf(builtin): inline fast paths for slice access and pointer values arrayIndex, sliceRange, sliceLen, pointerValue and copyElement are called for nearly every slice element access and pointer dereference, and each tried a long chain of representations. As large functions, engines did not inline them, so every element access in a hot loop was a call whose property loads saw every kind of slice in the program. Split each into a small fast path for the common representations (a window object, a typed slice or a plain array; a variable reference or a struct; a value that is not an object) and a slow function with the existing code for the others. V8 now inlines them into hot loops, where the window that sliceRange makes for an array also need not be allocated. In a benchmark of tensor element access (Cogent Lab tensor.Value with three indexes) under Node 26, an access takes 23 ns instead of 33 ns, plain indexing of a []int field in a loop 17 ns instead of 39 ns, and of a []float32 10 ns instead of 15 ns. Signed-off-by: Randall C. O'Reilly --- gs/builtin/builtin.ts | 27 ++++++++++------ gs/builtin/slice.ts | 74 ++++++++++++++++++++++++++++++++++++------- gs/builtin/type.ts | 8 ++++- 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/gs/builtin/builtin.ts b/gs/builtin/builtin.ts index 8f5537a97..228bfc72d 100644 --- a/gs/builtin/builtin.ts +++ b/gs/builtin/builtin.ts @@ -255,22 +255,29 @@ export function genericPointerRef( * variable's address. */ export function pointerValue(value: T | VarRef | null | undefined): T { + // A variable reference holds its value, and a struct that is not a boxed + // pointer is its own target. These property reads are much faster than + // the type and 'in' checks of pointerValueSlow, on the many kinds of + // objects seen here, and keep this function small enough to be inlined. + if (value !== null && value !== undefined) { + if ((value as any).__isVarRef === true) { + return (value as VarRef).value + } + if ((value as any).__goValue === undefined) { + return value as T + } + } + return pointerValueSlow(value) +} + +// pointerValueSlow is pointerValue for nil and boxed pointers. +function pointerValueSlow(value: T | VarRef | null | undefined): T { if (value === null || value === undefined) { runtimePanic( 'runtime error: invalid memory address or nil pointer dereference', ) } - // A variable reference holds its value, and a struct that is not a boxed - // pointer is its own target. These property reads are much faster than - // the type and 'in' checks below, on the many kinds of objects seen here. - if ((value as any).__isVarRef === true) { - return (value as VarRef).value - } - if ((value as any).__goValue === undefined) { - return value as T - } - // A boxed pointer is unwrapped to its target. const boxed = typeof value === 'object' && value !== null && '__goValue' in value ? diff --git a/gs/builtin/slice.ts b/gs/builtin/slice.ts index 6a34952b9..fcbcf7c2d 100644 --- a/gs/builtin/slice.ts +++ b/gs/builtin/slice.ts @@ -123,6 +123,22 @@ export function sliceRange( ): SliceRange export function sliceRange( slice: Slice | TypedSlice | null | undefined, +): SliceRange | SliceRange { + // A window object is its own window, and a plain array is a window over + // itself. They are handled here, in a function small enough for engines + // to inline, which also lets them avoid allocating the window of an array. + if (slice instanceof GoSlice) { + return slice + } + if (Array.isArray(slice) && (slice as any).__meta__ === undefined) { + return { backing: slice, offset: 0, length: slice.length } + } + return sliceRangeSlow(slice) +} + +// sliceRangeSlow is sliceRange for nil, typed and proxied slices. +function sliceRangeSlow( + slice: Slice | TypedSlice | null | undefined, ): SliceRange | SliceRange { // A nil slice has no elements. if (slice === null || slice === undefined) { @@ -1325,25 +1341,29 @@ export function arrayToSlice( export function sliceLen( slice: Slice | unknown[] | TypedSlice | null | undefined, ): number { - // A nil slice has no elements. - if (slice === null || slice === undefined) { - return 0 - } - - // A window object and a plain array are the common cases, tested first - // without reading __meta__, which is slow on the many kinds of objects - // seen here. An array proxy (a window viewed as a Go array) has __meta__. + // A window object, a plain array and a typed slice are the common cases, + // tested here without reading __meta__, which is slow on the many kinds of + // objects seen here, in a function small enough for engines to inline. An + // array proxy (a window viewed as a Go array) has __meta__. if (slice instanceof GoSlice) { return slice.length } + if (ArrayBuffer.isView(slice)) { + return (slice as TypedSlice).length + } if (Array.isArray(slice) && (slice as any).__meta__ === undefined) { return slice.length } + return sliceLenSlow(slice) +} - // A typed slice is a view of exactly its elements, whose window, if any, - // has the same length. - if (isTypedSlice(slice)) { - return slice.length +// sliceLenSlow is sliceLen for nil slices, proxies and windowed arrays. +function sliceLenSlow( + slice: Slice | unknown[] | TypedSlice | null | undefined, +): number { + // A nil slice has no elements. + if (slice === null || slice === undefined) { + return 0 } // A slice proxy knows its length. @@ -2332,6 +2352,36 @@ export function arrayIndex< | TypedSlice | null | undefined, +>(collection: C, index: number): ArrayIndexValue { + // An index in range of a window object, a typed slice or a plain array is + // read here, in a function small enough for engines to inline into hot + // loops; arrayIndexSlow handles the other representations and panics. + if (collection instanceof GoSlice) { + if (index >= 0 && index < collection.length) { + return collection.backing[collection.offset + index] as ArrayIndexValue + } + } else if (ArrayBuffer.isView(collection)) { + if (index >= 0 && index < (collection as TypedSlice).length) { + return (collection as TypedSlice)[index] as ArrayIndexValue + } + } else if (Array.isArray(collection) && (collection as any).__meta__ === undefined) { + if (index >= 0 && index < collection.length) { + return collection[index] as ArrayIndexValue + } + } + return arrayIndexSlow(collection, index) +} + +// arrayIndexSlow is arrayIndex for the representations and indexes that its +// fast path does not handle. +function arrayIndexSlow< + C extends + | Slice + | SliceProxy + | unknown[] + | TypedSlice + | null + | undefined, >(collection: C, index: number): ArrayIndexValue { if (collection === null || collection === undefined) { outOfRangeIndex(index, 0) diff --git a/gs/builtin/type.ts b/gs/builtin/type.ts index b953c48d2..31fd9f796 100644 --- a/gs/builtin/type.ts +++ b/gs/builtin/type.ts @@ -1948,10 +1948,16 @@ function markClonedStructValue(copy: T): T { // value unchanged. A marked struct with no clone method is returned unchanged // rather than panicking. export function copyElement(value: T): T { - // A value that is not an object is copied by value. + // A value that is not an object is copied by value, here in a function + // small enough for engines to inline; copyObjectElement copies the others. if (typeof value !== 'object' || value === null) { return value } + return copyObjectElement(value) +} + +// copyObjectElement is copyElement for a value that is an object. +function copyObjectElement(value: T & object): T { // A struct value of a class is copied as its class copies it. if ((value as any)[structValueMarker] === true) {