Skip to content

perf(python): read union case fields directly instead of rebuilding Array#4725

Open
MangelMaxime wants to merge 1 commit into
mainfrom
perf/python-read-union-cases
Open

perf(python): read union case fields directly instead of rebuilding Array#4725
MangelMaxime wants to merge 1 commit into
mainfrom
perf/python-read-union-cases

Conversation

@MangelMaxime

Copy link
Copy Markdown
Member

Summary

When working on Scriptorium, I discovered that Python generated code is slower than JS for union-heavy workloads.

One of the main reasons is that union case field reads (x.fields[i], emitted for every pattern match) went through Union.fields, a @property that rebuilds a full Array from getattr() calls on every access. Even if JavaScript does a similar thing, in that case it stores fields once at construction, while Python rebuilds it every time.

In my codebase, it resulted in union.fields being called (~8M calls) when profiling.

Amounting to ~5sec of the run time.

Before:

def sum(t: Tree) -> int32:
    match t.tag:
        case 1:
            return sum(t.fields[0]) + sum(t.fields[1])
        case _:
            return t.fields[0]

After:

def sum(t: Tree) -> int32:
    match t.tag:
        case 1:
            return sum(cast(Tree_Node, t).left_) + sum(cast(Tree_Node, t).right_)
        case _:
            return cast(Tree_Leaf, t).value_

The drawback I found from this change is that we now need to cast the union to the case class type before reading its fields. Otherwise, Pyright will complain that the "field property" (ex left_, item1, etc. ) is not defined on the union type.

Benchmark

I made a small benchmark to test the performance difference between the two approaches using something like the following code:

type Tree =
    | Leaf of value: int
    | Node of left: Tree * right: Tree

let rec sum (t: Tree) : int =
    match t with
    | Leaf v -> v
    | Node (l, r) -> sum l + sum r

Results:

nodes before after speedup
2,047 2.24s 0.22s 10.2x
65,535 3.86s 0.56s 6.8x
2,097,151 4.51s 0.70s 6.4x

@MangelMaxime MangelMaxime requested a review from dbrattli July 6, 2026 13:41
@github-actions

github-actions Bot commented Jul 6, 2026

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

@MangelMaxime MangelMaxime force-pushed the perf/python-read-union-cases branch from 5a140d9 to ec38a85 Compare July 6, 2026 21:09
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