Skip to content

perf(python): back int64 arrays with typed storage - #4864

Merged
dbrattli merged 1 commit into
mainfrom
perf/python-typed-int64-arrays
Jul 29, 2026
Merged

perf(python): back int64 arrays with typed storage#4864
dbrattli merged 1 commit into
mainfrom
perf/python-typed-int64-arrays

Conversation

@dbrattli

Copy link
Copy Markdown
Collaborator

TypedArrayCompatible in the Python Replacements.fs excluded Int64/UInt64
with a comment inherited from the JavaScript target:

// Don't use typed array for int64 until we remove our int64 polyfill
// and use JS BigInt to represent int64

That constraint has never applied to Python. The Rust core has backed these with
NativeArray::Int64(Vec<i64>) / UInt64(Vec<u64>) all along, and
Int64ArrayCons / UInt64ArrayCons are exported from array_.py — nothing had
ever emitted them.

The visible effect: Array.map over an int64[] emitted map(f, xs, None) and
fell back to boxed Generic storage, keeping one wrapper object per element.
Only array literals and Seq.toArray specialized, because those go through the
typed constructor rather than a library call — which is also why it looked fine
in passing.

Effect

For int64 this is a win on both axes rather than a tradeoff, because int64
values are still pyo3 wrappers, so the boxing that specialization avoids is
real. 1000 elements, release core:

storage a == b sum(a) a[500]
Int64 0.09 µs 17.6 µs 22.5 ns
Generic 8.53 µs 33.6 µs 20.0 ns

Equality is 95× because Vec<i64> == Vec<i64> compares entirely in Rust, where
Generic walks element-by-element through Python. Memory drops from an 8-byte
pointer plus a wrapper object per element to a flat 8 bytes.

Storage kind before → after, via quicktest:

                    before     after
literal             Int64      Int64
Seq.toArray         Int64      Int64
Array.init          Generic    Int64
Array.zeroCreate    Generic    Int64
Array.map           Generic    Int64
Array.filter        Generic    Int64
Array.append        Generic    Int64
Array.rev           Generic    Int64
Array.sort          Generic    Int64
uint64 Array.map    Generic    UInt64

Note on scope

Typed storage extracts to i64/u64 and raises if the value does not fit,
where Generic accepts anything. For a statically-typed int64[] that is
unreachable, and the boundary values are covered by the new tests — but it is a
tightening, not a pure no-op, so it is worth knowing about.

This does not close the remaining specialization holes: List.toArray,
Array.ofSeq and Set.toArray still produce Generic, because those library
functions erase 'T before they allocate and there is no cons for the call site
to pass. Fixing that means threading a parameter through F# sources shared with
other targets, so it is deliberately left out of here.

Test plan

  • Python target suite with --force-fable-library: 2445 passed (was 2442)
  • New TestArray.fs coverage — the file had no int64 tests at all, which
    is why this went unnoticed. Asserted against .NET first (228 array tests
    pass on .NET): value round-tripping through the storage, boundary values
    (Int64.MinValue/MaxValue, UInt64.MaxValue) that would expose a botched
    extraction, and structural equality — the operation the specialization is for
  • Quicktest verification of the storage kinds above, with values intact
    including Int64.MaxValue round-tripping as 9223372036854775807

🤖 Generated with Claude Code

`TypedArrayCompatible` excluded Int64 and UInt64 with a comment inherited from
the JavaScript target — "don't use typed array for int64 until we remove our
int64 polyfill and use JS BigInt". That constraint has never applied to Python.
The Rust core has backed these with `Vec<i64>`/`Vec<u64>` all along and
`Int64ArrayCons`/`UInt64ArrayCons` are exported from `array_.py`; nothing had
ever emitted them.

So `Array.map` over an `int64[]` passed `None` as the cons and fell back to
boxed `Generic` storage, keeping one wrapper object per element. Only array
literals and `Seq.toArray` specialized, because those go through the typed
constructor rather than a library call.

For int64 this is a win on every axis, since the values are still pyo3 wrappers
and the boxing that specialization avoids is real (1000 elements):

    storage    a == b     sum(a)
    Int64      0.09 us    17.6 us
    Generic    8.53 us    33.6 us

Equality is 95x because `Vec<i64> == Vec<i64>` compares entirely in Rust where
`Generic` walks element-by-element through Python.

`TestArray.fs` had no int64 coverage at all, which is why this went unnoticed.
The new tests assert value round-tripping through the storage, the boundary
values that would expose a botched i64/u64 extraction, and structural equality.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Python Type Checking Results (Pyright)

Metric Value
Total errors 34
Files with errors 4
Excluded files 4
New errors ✅ No
Excluded files with errors (4 files)

These files have known type errors and are excluded from CI. Remove from pyrightconfig.ci.json as errors are fixed.

File Errors Status
temp/tests/Python/test_hash_set.py 18 Excluded
temp/tests/Python/test_applicative.py 12 Excluded
temp/tests/Python/test_nested_and_recursive_pattern.py 2 Excluded
temp/tests/Python/fable_modules/thoth_json_python/encode.py 2 Excluded

@dbrattli
dbrattli merged commit 78a5d47 into main Jul 29, 2026
42 checks passed
@dbrattli
dbrattli deleted the perf/python-typed-int64-arrays branch July 29, 2026 16:12
dbrattli added a commit that referenced this pull request Jul 29, 2026
Brings in #4864 (typed int64 array storage) and the dependabot bumps. No
conflicts — #4864 touches the array cons table in Python's Replacements.fs and
TestArray.fs, neither of which this branch modifies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant