You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf: u8[i]/buf[i] on a Uint8Array or Buffer parameter is 53× slower than Node (out-of-line js_uint8array_get/set probing the buffer registries) #10515
Found by the package performance audit (real npm packages compiled from source, profiled against Node 26.5.1) and
re-measured on Perry 7661bc0 (v0.5.1589), Linux x64. Perry implements Uint8Array and Buffer as buffer-registry
objects, not as a typed-array kind. When the receiver is not a proven local (a parameter, a closure capture, or any
untyped value), no inline lane admits them. Every element read and write is then a runtime call that walks the
thread-local buffer registries.
nanoid's pool refill, buffer[i] = charCodes[buffer[i] & mask], is 114× slower than Node. The same loop over a Uint16Array runs 5× fewer instructions than the Uint8Array version.
These are medians of 3 runs on a shared, loaded host. Instruction counts do not depend on host load, so they are the
reliable figure. The instruction totals are whole-process instructions:u and include the N/5 warm-up. The
per-element figure divides by 78.6 M element steps. N = 2,000 refills of a 32,768-element array.
variant
Node loop ms
Perry loop ms
ratio
Perry instructions (per element)
Node wall
Perry wall
u8 (Uint8Array-typed params)
83.9
4,413
53×
55.87 G (710)
244 ms
5,307 ms
buffer (Buffer through the same Uint8Array params)
78.9
4,174
53×
55.87 G (710)
227 ms
5,098 ms
nanoid (closure-captured charCodes + let mask, Buffer)
48.1
5,505
114×
103.84 G (1,320)
140 ms
7,033 ms
u8any (untyped params, Uint8Array)
81.6
15,804
194×
193.03 G (2,455)
261 ms
18,968 ms
u16 (Uint16Array-typed params, control)
53.9
566
10.5×
11.23 G (143)
149 ms
713 ms
u16any (untyped params, Uint16Array, control)
83.7
2,423
29×
29.16 G (371)
240 ms
2,937 ms
Checksums are identical in all variants. Changing only the element kind from Uint16 to Uint8 costs 5× the instructions
with typed params and 6.6× with untyped params.
u8 profile (compiled loop 2.9 %):
symbol
share
is_registered_buffer_slow
35.0 %
js_uint8array_get
24.0 %
buffer_data
15.1 %
js_uint8array_set
11.8 %
foreign_backing
8.2 %
u8any profile:
symbol
share
is_registered_buffer_slow
20.2 %
js_dyn_index_get
13.4 %
is_registered_buffer
10.7 %
js_dyn_index_set_strict
10.1 %
js_dynamic_bitand + fmod + trunc
10.5 %
is_non_indexed_buffer_view
6.2 %
js_packed_arraylike_index_get
5.7 %
is_shared_sab
3.9 %
buffer_data
3.1 %
is_temporal_cell_addr
2.4 %
nanoid profile:
symbol
share
is_registered_buffer_slow
17.5 %
buffer_data
10.6 %
typed_array_get_numeric_index
7.9 %
js_dynamic_bitand
7.9 %
js_uint8array_get
7.1 %
foreign_backing
6.2 %
js_uint8array_index_get_value
6.1 %
js_uint8array_set
5.7 %
is_uint8array_buffer_slow
5.1 %
js_object_get_index_polymorphic
4.1 %
fmod + trunc
6.6 %
u16 profile: 93 % of samples are in the compiled loop (inline load and store).
Impact
Shares below come from the audit profiles taken on v0.5.1587.
nanoid 6.0.0 (nanoid() ×10M, 48× Node):
Typed-array/Buffer element access is about 67 % of Perry time, and the polymorphic index get another 3.5 %. The
objects-group attribution puts the construct at about 86 %.
The trigger is buffer[i] = charCodes[buffer[i] & mask] (index.js:100).
uuid 14.0.1: typed arrays are 12 % of Perry time, about 23 % including dispatch (byteToHex[arr[i]], Uint8Array.of). is_registered_buffer_slow is 8.0 % self.
@noble/hashes 2.2.0:
blake3: typed arrays are 27.0 % of Perry time (msg[offset + s[j++]] with a Uint8Array permutation parameter).
pg 8.22.0:is_registered_buffer_slow is 2.07 % self (io-group profile).
Mechanism
Verified unless marked (inferred).
Declared Uint8Array/Buffer receiver.
lower_uint8array_get_i32 (crates/perry-codegen/src/expr/arrays_finds.rs:229-275) first tries lower_buffer_load (crates/perry-codegen/src/expr/buffer_access.rs:458).
That needs a tracked buffer-view descriptor for a local (lower_buffer_access_proof, buffer_access.rs:295-300). It
also declines closure-captured buffers (buffer_access.rs:323). A parameter never has such a descriptor (inferred).
When the proof is missing, the lowering emits one call per element: js_uint8array_get (arrays_finds.rs:267) or js_uint8array_set (arrays_finds.rs:1129).
The runtime path (js_uint8array_get, crates/perry-runtime/src/typedarray/access.rs:650-679). A lookup_typed_array_kind miss is the normal case for a Uint8Array. From there:
is_registered_buffer_slow (header.rs:549): a thread-local RefCell<HashSet> probe plus the external and
shared-SAB registries.
js_buffer_get.
buffer_data (header.rs:1274): view::lookup, plus the foreign_backing thread-local map (header.rs:1067).
js_uint8array_set (access.rs:735) has the same shape.
Why Uint16Array is fast. It is a real typed-array kind, so it gets the inline guarded element lane: kind cache, PERRY_TA_VIEW_GUARD, bounds check, load/store.
Untyped receivers.js_packed_arraylike_index_get, js_dyn_index_get and js_dyn_index_set_strict ask the same
registries, plus is_non_indexed_buffer_view, is_shared_sab and is_temporal_cell_addr, on every element (profile).
Found by the package performance audit (real npm packages compiled from source, profiled against Node 26.5.1) and
re-measured on Perry 7661bc0 (v0.5.1589), Linux x64. Perry implements
Uint8ArrayandBufferas buffer-registryobjects, not as a typed-array kind. When the receiver is not a proven local (a parameter, a closure capture, or any
untyped value), no inline lane admits them. Every element read and write is then a runtime call that walks the
thread-local buffer registries.
nanoid's pool refill,
buffer[i] = charCodes[buffer[i] & mask], is 114× slower than Node. The same loop over aUint16Arrayruns 5× fewer instructions than theUint8Arrayversion.Reproduction
bench.ts(27 lines):Measurements
These are medians of 3 runs on a shared, loaded host. Instruction counts do not depend on host load, so they are the
reliable figure. The instruction totals are whole-process
instructions:uand include the N/5 warm-up. Theper-element figure divides by 78.6 M element steps. N = 2,000 refills of a 32,768-element array.
Uint8Array-typed params)Bufferthrough the sameUint8Arrayparams)charCodes+let mask, Buffer)Uint8Array)Uint16Array-typed params, control)Uint16Array, control)Checksums are identical in all variants. Changing only the element kind from Uint16 to Uint8 costs 5× the instructions
with typed params and 6.6× with untyped params.
u8profile (compiled loop 2.9 %):is_registered_buffer_slowjs_uint8array_getbuffer_datajs_uint8array_setforeign_backingu8anyprofile:is_registered_buffer_slowjs_dyn_index_getis_registered_bufferjs_dyn_index_set_strictjs_dynamic_bitand+fmod+truncis_non_indexed_buffer_viewjs_packed_arraylike_index_getis_shared_sabbuffer_datais_temporal_cell_addrnanoidprofile:is_registered_buffer_slowbuffer_datatyped_array_get_numeric_indexjs_dynamic_bitandjs_uint8array_getforeign_backingjs_uint8array_index_get_valuejs_uint8array_setis_uint8array_buffer_slowjs_object_get_index_polymorphicfmod+truncu16profile: 93 % of samples are in the compiled loop (inline load and store).Impact
Shares below come from the audit profiles taken on v0.5.1587.
nanoid()×10M, 48× Node):objects-group attribution puts the construct at about 86 %.
is_registered_buffer_slow30.9 %,buffer_data14.8 %,is_uint8array_buffer_slow9.1 %.buffer[i] = charCodes[buffer[i] & mask](index.js:100).byteToHex[arr[i]],Uint8Array.of).is_registered_buffer_slowis 8.0 % self.msg[offset + s[j++]]with aUint8Arraypermutation parameter).is_registered_buffer_slowis 2.07 % self (io-group profile).Mechanism
Verified unless marked (inferred).
Declared
Uint8Array/Bufferreceiver.lower_uint8array_get_i32(crates/perry-codegen/src/expr/arrays_finds.rs:229-275) first trieslower_buffer_load(crates/perry-codegen/src/expr/buffer_access.rs:458).lower_buffer_access_proof, buffer_access.rs:295-300). Italso declines closure-captured buffers (buffer_access.rs:323). A parameter never has such a descriptor (inferred).
js_uint8array_get(arrays_finds.rs:267) orjs_uint8array_set(arrays_finds.rs:1129).The runtime path (
js_uint8array_get,crates/perry-runtime/src/typedarray/access.rs:650-679). Alookup_typed_array_kindmiss is the normal case for a Uint8Array. From there:is_registered_buffer(crates/perry-runtime/src/buffer/header.rs:485).is_registered_buffer_slow(header.rs:549): a thread-localRefCell<HashSet>probe plus the external andshared-SAB registries.
js_buffer_get.buffer_data(header.rs:1274):view::lookup, plus theforeign_backingthread-local map (header.rs:1067).js_uint8array_set(access.rs:735) has the same shape.Why
Uint16Arrayis fast. It is a real typed-array kind, so it gets the inline guarded element lane: kind cache,PERRY_TA_VIEW_GUARD, bounds check, load/store.Untyped receivers.
js_packed_arraylike_index_get,js_dyn_index_getandjs_dyn_index_set_strictask the sameregistries, plus
is_non_indexed_buffer_view,is_shared_sabandis_temporal_cell_addr, on every element (profile).nanoid's closure. The captured
let maskturnsb[i] & maskintojs_dynamic_bitand(perf: bitwise operators on destructured,number-annotated or untyped values are 56× slower than Node (a js_dynamic_bit* call per operator, ToInt32 via software fmod) #10511). The capturedcharCodesread goes throughjs_object_get_index_polymorphic.What fast looks like
Uint8Array/Bufferreceivers a guarded inline byte load/store equivalent tothe one
Uint16Arrayalready gets:load i8/store i8atheader + size_of::<BufferHeader>().u8andbufferat or below theu16instruction count (≤ ~150 per element);u8any≈u16any.With perf: bitwise operators on destructured,
number-annotated or untyped values are 56× slower than Node (a js_dynamic_bit* call per operator, ToInt32 via software fmod) #10511 also fixed, the nanoid refill should be ≤3× Node.Notes
as "not indexable". It does not track this element-access cost.
access.
untyped receivers remain out of line.
number-annotated or untyped values are 56× slower than Node (a js_dynamic_bit* call per operator, ToInt32 via software fmod) #10511 is the dynamic&.