Skip to content

Comments

Adopt mark-sweep GC and expand benchmarks#537

Open
Jim8y wants to merge 18 commits intomasterfrom
feature/mark-sweep-default
Open

Adopt mark-sweep GC and expand benchmarks#537
Jim8y wants to merge 18 commits intomasterfrom
feature/mark-sweep-default

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


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.

}

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;
Copy link
Contributor

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
Contributor

Good job.

Conflicts now.

@roman-khimov
Copy link
Contributor

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

ajara87
ajara87 previously approved these changes Dec 2, 2025
@Jim8y
Copy link
Contributor Author

Jim8y commented Dec 6, 2025

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

still have that in mind, but not a hurry~~

@Jim8y
Copy link
Contributor Author

Jim8y commented Dec 7, 2025

Well, from the unit tests result, the RC still works the same. will request for a full scale test upon mainnet from NGD

@superboyiii
Copy link
Member

superboyiii commented Dec 8, 2025

It fails on Block index 290071, engine.FaultException. I use v3.8.2 merged with this.

@superboyiii
Copy link
Member

superboyiii commented Dec 8, 2025

Storage difference causes not enough GAS to burn on 290071, the first incompatible block is 287782.
6ab44e22-0268-4b6f-850d-af965525ceed
7f07dfe8-5fe6-44c7-b6b6-2d55038efac8

@shargon
Copy link
Member

shargon commented Dec 29, 2025

Storage difference causes not enough GAS to burn on 290071, the first incompatible block is 287782. 6ab44e22-0268-4b6f-850d-af965525ceed 7f07dfe8-5fe6-44c7-b6b6-2d55038efac8

@Jim8y Did you check it? It should be good if we use this new system

ajara87
ajara87 previously approved these changes Dec 29, 2025
@ajara87 ajara87 self-requested a review December 29, 2025 16:43
@ajara87 ajara87 dismissed their stale review December 29, 2025 16:44

approved by mistake

@shargon
Copy link
Member

shargon commented Jan 8, 2026

Ping @Jim8y

@Jim8y
Copy link
Contributor Author

Jim8y commented Jan 24, 2026

Scenario MarkSweep Mean ReferenceCounter Mean Speedup
NestedArrays 137.3 μs 231.2 μs ~1.7x
DenseCycles 24,648 μs 47,912 μs ~1.9x
StackChurn 428.5 μs 684.9 μs ~1.6x
MapHeavy 3,283 μs 5,896 μs ~1.8x

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

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


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

@Jim8y
Copy link
Contributor Author

Jim8y commented Jan 24, 2026

@shargon , i tested and added fuzzing for parity, all passed (400,000 steps of fuzzing, running for over 30 minutes), superboy will run on testnet/mainnet next tuesday.

@shargon
Copy link
Member

shargon commented Jan 24, 2026

@shargon , i tested and added fuzzing for parity, all passed (400,000 steps of fuzzing, running for over 30 minutes), superboy will run on testnet/mainnet next tuesday.

Nice!!! Good work!

@Wi1l-B0t
Copy link
Contributor

@shargon , i tested and added fuzzing for parity, all passed (400,000 steps of fuzzing, running for over 30 minutes), superboy will run on testnet/mainnet next tuesday.

Good job.

@roman-khimov
Copy link
Contributor

Scenario MarkSweep Mean ReferenceCounter Mean Speedup

Unexpected, interesting. I still think GC is not needed at all here and I suspect the main difference comes from neo-project/neo#3107, but this needs to be checked.

@roman-khimov
Copy link
Contributor

@Jim8y, are you sure you're testing it correctly? Have you tried any real transactions or VM code with different implementations? The way I see benchmark workloads are structured you're doing a lot of operations (potentially even going beyond MaxStackSize) and then basically a single GC pass. It's good to compare different GCs, but not exactly correct to compare GC-less RC. It can have somewhat more per-operation overhead, but then CheckZeroReferred() is free, nothing to collect.

The way real code is executed you do have to GC at least from time to time, the model we have requires ensuring VM limits are not broken at every instruction and the only thing preventing GC from being run for all instructions is neo-project/neo#3107. Then it's the question of how often it's being triggered in various scenarios, practical or DoS ones. Likely there are scripts that trigger it more often and there are scripts that trigger it less often, but expecting a single GC invocation for VM run is somewhat optimistic to me.

@Jim8y
Copy link
Contributor Author

Jim8y commented Jan 27, 2026

@roman-khimov thank you very much for your review. I also have the same concern, that is why i requested superboy to do a full scale testnet and mainnnet state consistency check. I personally also run upon mainnet data multiple times.

This also reminds me the pain of not having a functioning state solution in neo, i am trying to convince Erik to bring your solution or another state solution back to neo.

@superboyiii
Copy link
Member

superboyiii commented Jan 28, 2026

Tested. Data by this vm is 100% compatible with mainnet.

@roman-khimov
Copy link
Contributor

a full scale testnet and mainnnet state consistency check

That's somewhat different. It certainly can be made compatible and @superboyiii proves it already is, but is it really faster than simpler RC scheme on real scripts is still unknown. I won't (and can't) block merging it, since it's an improvement over the previous GC anyway, but there is an alternative implementation that can be even better (or not!) and I'd rather carefully choose between the two and forget about this topic for some years. The outcome is not predetermined, as I said neo-project/neo#3107 changed the game somewhat, but I fear specifically crafted scripts can show weaknesses of GC that plain RC doesn't have.

not having a functioning state solution in neo

Just merge neo-project/neo#4161 into N3 and that's it. We have absolutely everything implemented for neo-project/neo#3463 in NeoGo (it can sync state from NeoFS right now), except this mechanism can't be trusted because of missing stateroot (oopsie).

@Jim8y
Copy link
Contributor Author

Jim8y commented Jan 28, 2026

a full scale testnet and mainnnet state consistency check

That's somewhat different. It certainly can be made compatible and @superboyiii proves it already is, but is it really faster than simpler RC scheme on real scripts is still unknown. I won't (and can't) block merging it, since it's an improvement over the previous GC anyway, but there is an alternative implementation that can be even better (or not!) and I'd rather carefully choose between the two and forget about this topic for some years. The outcome is not predetermined, as I said neo-project/neo#3107 changed the game somewhat, but I fear specifically crafted scripts can show weaknesses of GC that plain RC doesn't have.

not having a functioning state solution in neo

Just merge neo-project/neo#4161 into N3 and that's it. We have absolutely everything implemented for neo-project/neo#3463 in NeoGo (it can sync state from NeoFS right now), except this mechanism can't be trusted because of missing stateroot (oopsie).

@roman-khimov , hi roman, dont worry , i will carry out real world mainnet synchronization speed testing to evaluate the performance. After this one is down, i will continue working to pick up previously NeoGo style GC, and do benchmark as well. Optimization will be my continuse work, and i am not in a hurry to merge this one as well.

}
continue;
}
if (parent is Map map)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (parent is Map map)
else if (parent is Map map)

Comment on lines +575 to +577
continue;
}
if (child is Buffer buffer)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
continue;
}
if (child is Buffer buffer)
}
else if (child is Buffer buffer)

Comment on lines +595 to +597
continue;
}
if (child is Buffer buffer)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
continue;
}
if (child is Buffer buffer)
}
else if (child is Buffer buffer)

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.

6 participants