Skip to content

Memory and peformance optimizations in interop.#2382

Merged
lahma merged 4 commits into
sebastienros:mainfrom
StringEpsilon:Optimizations
Apr 6, 2026
Merged

Memory and peformance optimizations in interop.#2382
lahma merged 4 commits into
sebastienros:mainfrom
StringEpsilon:Optimizations

Conversation

@StringEpsilon

Copy link
Copy Markdown
Contributor

Disclaimer: I don't know enough about jint to be sure that these optimizations don't cause any side effects, hence this is a draft. But all the tests pass on my machine so I think it's working.

Background: I was investigating the performance of an application I help maintain and noticed that Jint allocates a LOT of doubles and bytes, which greatly increases the GC pressure we experience as we execute JS very frequently on the main loop.

The change in MethodInfoFunction.cs (commit "MethodInfoFunction.Call: Cache argument object") might be a performance regression for some specific cases, but just about cuts in half the allocations for double I see with dotMemory.

After that, I experimented with using a custom type converter to use a uint8array for byte[] instead of a regular array of doubles. That turned out to be slower because of how the ToNativeArray<T>() is implemented. Adding a fast path for uint8 arrays that simply returns the internal buffer array greatly reduces allocations and negates the performance penalty I saw for using the custom converter.

Related is the change to Uint8ArrayConstructor: The previous implementation seems to be a lot slower (and with more allocations) than simply creating a copy of the ROS.

The latter 2 changes may cause some breakage, as I'm not sure that they work for all uint8 arrays and the respective construction cases. But I hope that my hasty changes can lead to some proper optimizations in these areas.

@lahma

lahma commented Apr 5, 2026

Copy link
Copy Markdown
Collaborator

Nice, thanks for bringing the PR. In these cases would love to see benchmarks added to benchmark project that simulate the software you are tweaking - would show the actual benefits.

@StringEpsilon

Copy link
Copy Markdown
Contributor Author

Benchmark code:

public class MemoryManager
{
    static byte[] _buffer = new byte[1000];
    public byte[] get_bytes(int start, int length) => _buffer.AsSpan(start, length).ToArray();
    public void fill(int start, byte[] data) => data.AsSpan().CopyTo(_buffer.AsSpan(start, data.Length));
}

public class Uint8Converter : IObjectConverter
{
    public bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result)
    {
        if (value is byte[] byteArray)
        {
            result = engine.Intrinsics.Uint8Array.Construct((ReadOnlySpan<byte>)byteArray);
            return true;
        }
        result = null;
        return false;
    }
}

[MemoryDiagnoser]
public class ByteArrayInteropBenchmark
{
    private Engine _stockEngine = new Engine(new Options()).SetValue("__memory", new MemoryManager());
    private Engine _uint8Engine;
	public ByteArrayInteropBenchmark()
    {
        var uint8Options = new Options();
        
        uint8Options.Interop.ObjectConverters.Add(new Uint8Converter());
        _uint8Engine = new Engine(uint8Options).SetValue("__memory", new MemoryManager());
    }

    [Benchmark]
    public void Stock() =>
        _stockEngine.Execute("""
            for(let i=0; i<10; i++) {
                var bytes = __memory.get_bytes(100*i, 10);
                __memory.fill(100*i, bytes);
            }
            """);

    [Benchmark]
    public void UInt8() =>
        _uint8Engine.Execute("""
            for(let i=0; i<10; i++) {
                var bytes = __memory.get_bytes(100*i, 10);
                __memory.fill(100*i, bytes);
            }
            """);
}

Results:

Method Mean Error StdDev Gen0 Gen1 Allocated
old Stock 15.110 us 0.076 us 0.071 us 2.7161 0.0610 44.57 KB
new Stock 14.622 us 0.0510 us 0.0452 us 2.4414 0.0610 40.04 KB
old UInt8 13.610 us 0.073 us 0.068 us 2.9297 0.0610 48.01 KB
new UInt8 9.220 us 0.0575 us 0.0538 us 1.3733 0.0305 22.62 KB

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to reduce GC pressure and improve interop performance in Jint by minimizing allocations during CLR invocation and by optimizing Uint8Array creation/conversion paths.

Changes:

  • Cache JsValue.ToObject() results in MethodInfoFunction.Call to avoid repeated conversions during overload resolution.
  • Optimize Uint8ArrayConstructor.Construct(ReadOnlySpan<byte>) by writing span data directly to the underlying buffer.
  • Add a fast-path in JsTypedArray.ToNativeArray<T>() for Uint8Arraybyte[] conversions.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
Jint/Runtime/Interop/MethodInfoFunction.cs Attempts to reduce repeated ToObject() conversions during interop method argument processing.
Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs Changes how Uint8Array is constructed from ReadOnlySpan<byte> to reduce per-element work.
Jint/Native/JsTypedArray.cs Adds a fast-path intended to avoid allocating/copying when converting Uint8Array to byte[].

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Jint/Runtime/Interop/MethodInfoFunction.cs
Comment thread Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs
Comment thread Jint/Native/JsTypedArray.cs
@StringEpsilon
StringEpsilon marked this pull request as ready for review April 6, 2026 11:44
I'll rebase this commit later.

@lahma lahma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@lahma
lahma enabled auto-merge (squash) April 6, 2026 12:41
@lahma
lahma merged commit 521474d into sebastienros:main Apr 6, 2026
4 checks passed
@StringEpsilon
StringEpsilon deleted the Optimizations branch April 7, 2026 19:27
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.

3 participants