Skip to content

ZJIT: write_byte in CodeBlock is slow #1073

Open

Description

@tekknolagi

We should be able to write chunks instead of doing a bunch of borrows, checks, and bookkeeping for every byte of emitted machine code

Activity

  1. supervacuus commented on Oct 16, 2026

    @supervacuus

    I looked into this by adding a bulk VirtualMemory::write_bytes() operation and letting CodeBlock::write_bytes() call it, instead of emitting slices byte by byte. It takes over the page-bounding from the single-byte implementation and returns written bytes along with the error on failure.

    A quick test on ARM64 shows that this reduces median LIR compilation time by 3–7% across the three 30k_* ruby-bench workloads, and total compilation time by 1–2.3%. I would be checking x86-64 next. Is this within the expected margin?

    Does this match what you intended here, or were you thinking more of a scoped/buffered writer (which seems especially suited for x86-64 instruction encoders, after a quick look) that also batches separate write_byte calls?

  2. tekknolagi commented on Oct 16, 2026

    @tekknolagi
    Author

    Both are acceptable, probably? I'm not sure. I would expect some amount of "page buffering" so to speak which should drop the check to at least 1 in 4k instead of once per byte. I would also expect a bulk API.

    This kind of change can be kind of dicy to implement cleanly. You are more than welcome to submit a PR (please let me know if you want to claim the issue and I'll assign it), but please ensure your expectations are set accordingly: there will be probably a decent amount of API review and it may not end up getting merged for one reason or another. But if it strikes the right balance, we would be overjoyed to merge/land such a PR.

  3. supervacuus commented on Oct 18, 2026

    @supervacuus

    Thank you for the quick response, @tekknolagi.

    Both are acceptable, probably? I'm not sure. I would expect some amount of "page buffering" so to speak which should drop the check to at least 1 in 4k instead of once per byte. I would also expect a bulk API.

    Yeah, I only started with the bulk-writer because I could quickly test ARM64 emission on my MBP. In the workloads I measured, all writes are either 4 or 8 bytes, so they could benefit immediately, but I was surprised by such gains from even these small writes.

    A writer that could amortize the fast-path overhead over multiple consecutive writes was an obvious next optimization, especially on x86-64 with many single-byte writes.

    So, I started with an explicit "CodeWriter" that holds the memory borrow and caches a validated writable span across calls. On span hits, this replaces repeated borrowing, page derivation, and per-byte call/result handling with a cheap span-size test, a store, and the cursor advance. For multibyte slices, I kept the bulk stores rather than repeated byte writes.

    This kind of change can be kind of dicy to implement cleanly. You are more than welcome to submit a PR (please let me know if you want to claim the issue and I'll assign it), but please ensure your expectations are set accordingly: there will be probably a decent amount of API review and it may not end up getting merged for one reason or another. But if it strikes the right balance, we would be overjoyed to merge/land such a PR.

    That's a fair warning, and I appreciate it. I will create a draft PR tomorrow that contains my current implementation state with a few numbers I gathered from running the 30k_* ruby-bench workloads. If you have time, I would appreciate feedback on the current API, any potential consequences, and whether the optimizations go in a direction you agree with.

    You can assign the issue to me; I think that the current implementation is at a point where a first review could make sense, and if no one else is currently looking into this, then that justifies at least a temporary claim :-)

  4. supervacuus commented on Oct 20, 2026

    @supervacuus

    @tekknolagi, only wanted to ping here that I created a PR: ruby#19430

    It got quite big (sorry), but most of it isn't behaviorally significant; a large part is type changes and related name changes (I wrestled with whether they should be in the initial PR).

    In any case, please don't take this as me being pushy. I just forgot the repos belong to different orgs, and I have no idea how notifications are set up to align PRs with related issues across orgs. So this is mostly bookkeeping, with no intent to signal any urgency.

  5. tekknolagi commented on Oct 20, 2026

    @tekknolagi
    Author

    We've seen it, don't worry—we're looking

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions