Skip to content

Read fields of a variable-free object without copying its type - #9259

Draft
cknitt wants to merge 2 commits into
masterfrom
object-fields-perf
Draft

cknitt wants to merge 2 commits into
masterfrom
object-fields-perf

Conversation

@cknitt

@cknitt cknitt commented Nov 7, 2026

Copy link
Copy Markdown
Member

Part of #9256 (field access on a large object type).

Each o["f"] instantiated the whole type of o, and the .cmt file stored every copy (writing it was the largest cost), so reading each field of an n-field object once was quadratic.

When the identifier's type is generic and has no type variables, an instance would be an identical copy. o then keeps the type itself, and only the field's type is instantiated, as before. filter_method doesn't change a type that has the field, and the shared type is never unified: the identifier isn't unified with the expected type, which is a fresh variable there. Types with variables are still copied. The check walks the type once and remembers the last type asked about, as reads of one object usually follow each other.

Measured with bsc on linux-arm64, allocated words and wall time:

Fields Before After
400 21.1M, 189 ms 3.4M, 34 ms
800 77.4M, 867 ms 6.5M, 62 ms
1600 297M, 3.95 s 12.9M, 136 ms

What remains is the linear scan of the field list per read.

New test: undefined_method_closed_object reads an existing and a missing field of a toplevel object, so the error goes through the new path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kbf3gK1dcjbe97yY1wRMgs

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

  • Speed up reading many fields of a large object, and shrink the .cmt files of such modules.

Semantic Diff

2 commits · Full page

Read fields of a variable-free object without copying its type

426355d · cknitt + Claude · Nov 07, 2026, 10:06 UTC

Why

Each o["f"] instantiated the whole type of o, and the .cmt file stored every copy, so reading each field of an object with n fields once cost O(n²): 800 fields allocated 77M words and took 0.87 s. They now allocate 6.5M words and take 0.06 s.

When the identifier's type is generic and has no type variables, a copy would be identical, so the identifier keeps the type itself and only the field's type is instantiated. Looking up a field that the type has doesn't change it. Types with variables are still copied.

✨ Type the object of a field read through its own identifier helper

41ad3b0 · cknitt + Claude · Nov 07, 2026, 19:05 UTC · new

The benchmarks showed every expression allocating a little more (up to 0.17%), because the new optional argument of type_expect was captured by the closure it builds for each expression. Identifiers are now typed by type_ident, which the field read calls directly for a bare identifier, so type_expect is as before.

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.74%. Comparing base (bf35764) to head (41ad3b0).

Files with missing lines Patch % Lines
compiler/ml/typecore.ml 93.33% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9259   +/-   ##
=======================================
  Coverage   81.74%   81.74%           
=======================================
  Files         457      457           
  Lines       65713    65729   +16     
=======================================
+ Hits        53715    53731   +16     
  Misses      11998    11998           
Files with missing lines Coverage Δ
compiler/ml/typecore.ml 89.18% <93.33%> (+0.06%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9259

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9259

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9259

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9259

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9259

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9259

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9259

commit: 41ad3b0

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Why:
    Each `o["f"]` instantiated the whole type of `o`, and the .cmt file
    stored every copy, so reading each field of an object with n fields
    once cost O(n²): 800 fields allocated 77M words and took 0.87 s. They
    now allocate 6.5M words and take 0.06 s.

When the identifier's type is generic and has no type variables, a copy
would be identical, so the identifier keeps the type itself and only the
field's type is instantiated. Looking up a field that the type has doesn't
change it. Types with variables are still copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gK1dcjbe97yY1wRMgs
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the object-fields-perf branch from 2537b0a to 426355d Compare November 7, 2026 18:06
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

🎉 Compiler benchmarks

Allocated words

In total: 456,989,425 → 439,257,959 (-3.88%).

🎉 Improvement: 1 benchmark allocates more than 1% less.

Benchmark Base Head Change
stress/ObjectFields.res 21,040,664 3,296,387 -84.33%

Instructions

In total: 10,226,962,471 → 9,405,550,072 (-8.03%).

🎉 Improvement: 1 benchmark executes more than 1% fewer instructions.

Benchmark Base Head Change
stress/ObjectFields.res 957,097,762 135,501,721 -85.84%

Pull request 41ad3b0 compared with base bf35764 in this run.

The benchmarks showed every expression allocating a little more (up to
0.17%), because the new optional argument of type_expect was captured by
the closure it builds for each expression. Identifiers are now typed by
type_ident, which the field read calls directly for a bare identifier, so
type_expect is as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gK1dcjbe97yY1wRMgs
Signed-off-by: Christoph Knittel <ck@cca.io>

This branch has not been deployed

No deployments
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