Skip to content

Conversation

@Jim8y
Copy link
Contributor

@Jim8y Jim8y commented Nov 15, 2025

Summary

  • switch default ExecutionEngine instances to MarkSweepReferenceCounter
  • add deterministic mark-sweep implementation and tests comparing it to the legacy counter
  • expand ReferenceCounter benchmarks with high-pressure scenarios to demonstrate performance gains

Testing

  • dotnet test
  • NEO_VM_BENCHMARK=1 dotnet run -c Release --project benchmarks/Neo.VM.Benchmarks --filter 'ReferenceCounterBenchmarks'

@Jim8y
Copy link
Contributor Author

Jim8y commented Nov 15, 2025

image

@shargon shargon requested a review from Copilot November 15, 2025 22:09
Copilot finished reviewing on behalf of shargon November 15, 2025 22:14
/// <param name="jumpTable">The jump table to be used.</param>
public ExecutionEngine(JumpTable? jumpTable = null)
: this(jumpTable, new ReferenceCounter(), ExecutionEngineLimits.Default) { }
: this(jumpTable, new MarkSweepReferenceCounter(), ExecutionEngineLimits.Default) { }
Copy link
Member

Choose a reason for hiding this comment

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

It's tested to be the same in mainnet and testnet?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will carry out the full scale test.


StackItem[] copyList = [.. _innerList];
List<StackItem> reverseList = [.. copyList.Reverse()];
List<StackItem> reverseList = [.. copyList.AsEnumerable().Reverse()];
Copy link
Member

Choose a reason for hiding this comment

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

It's required?

Copy link

Copilot AI left a comment

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 introduces a mark-sweep garbage collection strategy as an alternative to the legacy reference counter, making it the new default for ExecutionEngine instances. The mark-sweep implementation aims to improve performance in high-pressure scenarios with complex circular references.

  • Adds MarkSweepReferenceCounter class implementing IReferenceCounter interface
  • Changes ExecutionEngine to use MarkSweepReferenceCounter by default
  • Adds comprehensive benchmarks comparing both implementations under various workload scenarios

Reviewed Changes

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

Show a summary per file
File Description
src/Neo.VM/MarkSweepReferenceCounter.cs New mark-sweep GC implementation using reachability analysis to detect and collect unreachable circular references
src/Neo.VM/ExecutionEngine.cs Updates default reference counter from ReferenceCounter to MarkSweepReferenceCounter
tests/Neo.VM.Tests/UT_ReferenceCounter.cs Refactors existing tests to use helper methods and adds tests comparing both counter implementations
src/Neo.VM/EvaluationStack.cs Adds explicit AsEnumerable() call for code clarity when reversing arrays
benchmarks/Neo.VM.Benchmarks/ReferenceCounterBenchmarks.cs New benchmark suite with three workload scenarios (nested arrays, dense cycles, stack churn) to measure performance differences

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

Comment on lines +235 to +240
using (ExecutionEngine engine = new())
{
engine.LoadScript(script);
Assert.AreEqual(VMState.HALT, engine.Execute());
Assert.AreEqual(expectedCount, engine.ReferenceCounter.Count);
}
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

The test is comparing two MarkSweepReferenceCounter instances instead of comparing MarkSweepReferenceCounter with the legacy ReferenceCounter. Since the default ExecutionEngine now uses MarkSweepReferenceCounter (changed in ExecutionEngine.cs line 89), the first engine also uses MarkSweepReferenceCounter.

To properly test compatibility between the two implementations, the first engine should explicitly use the legacy ReferenceCounter:

using (CounterEngine engine = new(new ReferenceCounter()))

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

That's true

Copy link
Member

Choose a reason for hiding this comment

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

}

private void Collect()
{
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

The pending stack is reused across multiple Collect() calls but is not explicitly cleared at the start of Collect(). While it should normally be empty after Mark() completes, if an exception occurs during a previous Mark() operation, leftover items could contaminate subsequent collections, leading to incorrect marking behavior.

Consider adding pending.Clear(); at the start of the Collect() method to ensure a clean state for each collection cycle.

Suggested change
{
{
pending.Clear();

Copilot uses AI. Check for mistakes.
@shargon
Copy link
Member

shargon commented Nov 15, 2025

Good job @Jim8y, speed up is almost 100%, and seems simpler than the previous algorithm


if (item is CompoundType compound)
{
referencesCount -= compound.SubItemsCount;

Choose a reason for hiding this comment

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

Add assert referencesCount > compound.SubItemsCount here?

@Wi1l-B0t
Copy link

Good job.

Conflicts now.

@roman-khimov
Copy link
Contributor

Have you compared it to neo-project/neo#3581?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants