diff --git a/benchmarks/Neo.VM.Benchmarks/ReferenceCounterBenchmarks.cs b/benchmarks/Neo.VM.Benchmarks/ReferenceCounterBenchmarks.cs new file mode 100644 index 00000000..04050617 --- /dev/null +++ b/benchmarks/Neo.VM.Benchmarks/ReferenceCounterBenchmarks.cs @@ -0,0 +1,159 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ReferenceCounterBenchmarks.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using Array = Neo.VM.Types.Array; + +namespace Neo.VM.Benchmarks; + +[SimpleJob] +[MemoryDiagnoser] +public class ReferenceCounterBenchmarks +{ + public enum Workload + { + NestedArrays, + DenseCycles, + StackChurn + } + + [Params(nameof(ReferenceCounter), nameof(MarkSweepReferenceCounter))] + public string Strategy { get; set; } = nameof(ReferenceCounter); + + [Params(Workload.NestedArrays, Workload.DenseCycles, Workload.StackChurn)] + public Workload Scenario { get; set; } + + [Params(32)] + public int RootCount { get; set; } + + [Params(4)] + public int Depth { get; set; } + + [Params(4)] + public int FanOut { get; set; } + + [Params(1024)] + public int Iterations { get; set; } + + [Benchmark] + public int ExecuteScenario() + { + return Scenario switch + { + Workload.NestedArrays => CollectNestedArrays(), + Workload.DenseCycles => CollectDenseCycles(), + Workload.StackChurn => CollectStackChurn(), + _ => throw new ArgumentOutOfRangeException(nameof(Scenario)) + }; + } + + private int CollectNestedArrays() + { + var counter = CreateCounter(); + var roots = new Array[RootCount]; + + for (int i = 0; i < RootCount; i++) + { + var root = new Array(counter); + counter.AddStackReference(root); + roots[i] = root; + BuildNested(root, Depth, counter); + } + + foreach (var root in roots) + counter.RemoveStackReference(root); + + return counter.CheckZeroReferred(); + } + + private void BuildNested(Array current, int depth, IReferenceCounter counter) + { + if (depth == 0) return; + + var child = new Array(counter); + current.Add(child); + child.Add(current); + + var sibling = new Array(counter); + current.Add(sibling); + sibling.Add(child); + + BuildNested(child, depth - 1, counter); + } + + private int CollectDenseCycles() + { + var counter = CreateCounter(); + var roots = new Array[RootCount]; + for (int i = 0; i < RootCount; i++) + { + roots[i] = new Array(counter); + counter.AddStackReference(roots[i]); + } + + foreach (var root in roots) + BuildDenseCycles(root, Depth, counter, roots); + + foreach (var root in roots) + counter.RemoveStackReference(root); + + return counter.CheckZeroReferred(); + } + + private void BuildDenseCycles(Array parent, int depth, IReferenceCounter counter, Array[] roots) + { + if (depth == 0) return; + for (int i = 0; i < FanOut; i++) + { + var child = new Array(counter); + parent.Add(child); + child.Add(parent); + if (i % 2 == 0) + child.Add(roots[(i + parent.Count) % roots.Length]); + BuildDenseCycles(child, depth - 1, counter, roots); + } + } + + private int CollectStackChurn() + { + var counter = CreateCounter(); + List window = new(); + for (int i = 0; i < Iterations; i++) + { + var node = new Array(counter); + counter.AddStackReference(node); + if (window.Count > 0) + node.Add(window[i % window.Count]); + window.Add(node); + if (window.Count > FanOut) + { + var victim = window[0]; + counter.RemoveStackReference(victim); + window.RemoveAt(0); + } + } + + foreach (var node in window) + counter.RemoveStackReference(node); + + return counter.CheckZeroReferred(); + } + + private IReferenceCounter CreateCounter() => Strategy switch + { + nameof(ReferenceCounter) => new ReferenceCounter(), + nameof(MarkSweepReferenceCounter) => new MarkSweepReferenceCounter(), + _ => throw new ArgumentOutOfRangeException(nameof(Strategy)) + }; +} diff --git a/src/Neo.VM/ExecutionEngine.cs b/src/Neo.VM/ExecutionEngine.cs index 3ec84101..211258bd 100644 --- a/src/Neo.VM/ExecutionEngine.cs +++ b/src/Neo.VM/ExecutionEngine.cs @@ -86,7 +86,7 @@ protected internal set /// /// The jump table to be used. public ExecutionEngine(JumpTable? jumpTable = null) - : this(jumpTable, new ReferenceCounter(), ExecutionEngineLimits.Default) { } + : this(jumpTable, new MarkSweepReferenceCounter(), ExecutionEngineLimits.Default) { } /// /// Initializes a new instance of the class diff --git a/src/Neo.VM/MarkSweepReferenceCounter.cs b/src/Neo.VM/MarkSweepReferenceCounter.cs new file mode 100644 index 00000000..e264b6ed --- /dev/null +++ b/src/Neo.VM/MarkSweepReferenceCounter.cs @@ -0,0 +1,171 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MarkSweepReferenceCounter.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM.Types; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Neo.VM; + +/// +/// Reference counter that performs a deterministic mark-sweep collection. +/// +public sealed class MarkSweepReferenceCounter : IReferenceCounter +{ + private readonly HashSet trackedItems = new(ReferenceEqualityComparer.Instance); + private readonly HashSet zeroReferred = new(ReferenceEqualityComparer.Instance); + private readonly Stack pending = new(); + private readonly HashSet marked = new(ReferenceEqualityComparer.Instance); + private readonly List unreachable = new(); + + private int referencesCount; + + /// + public int Count => referencesCount; + + /// + public void AddZeroReferred(StackItem item) + { + if (zeroReferred.Add(item) && NeedTrack(item)) + trackedItems.Add(item); + } + + /// + public void AddReference(StackItem item, CompoundType parent) + { + referencesCount++; + if (!NeedTrack(item)) return; + + Track(item); + + item.ObjectReferences ??= new Dictionary(ReferenceEqualityComparer.Instance); + if (!item.ObjectReferences.TryGetValue(parent, out var entry)) + { + entry = new StackItem.ObjectReferenceEntry(parent); + item.ObjectReferences.Add(parent, entry); + } + entry.References++; + } + + /// + public void AddStackReference(StackItem item, int count = 1) + { + referencesCount += count; + if (!NeedTrack(item)) return; + + Track(item); + item.StackReferences += count; + zeroReferred.Remove(item); + } + + /// + public int CheckZeroReferred() + { + if (zeroReferred.Count == 0 || trackedItems.Count == 0) + return referencesCount; + + Collect(); + return referencesCount; + } + + /// + public void RemoveReference(StackItem item, CompoundType parent) + { + referencesCount--; + if (!NeedTrack(item)) return; + + item.ObjectReferences![parent].References--; + if (item.StackReferences == 0) + zeroReferred.Add(item); + } + + /// + public void RemoveStackReference(StackItem item) + { + referencesCount--; + if (!NeedTrack(item)) return; + + if (--item.StackReferences == 0) + zeroReferred.Add(item); + } + + /// + /// Only compound types and buffers require tracking because they own other items or pooled memory. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedTrack(StackItem item) => item is CompoundType or Buffer; + + private void Track(StackItem item) + { + if (trackedItems.Add(item)) + zeroReferred.Add(item); + } + + private void Collect() + { + marked.Clear(); + unreachable.Clear(); + + foreach (var item in trackedItems) + { + if (item.StackReferences > 0) + Mark(item); + } + + foreach (var item in trackedItems) + { + if (!marked.Contains(item) && zeroReferred.Contains(item)) + unreachable.Add(item); + } + + foreach (var item in unreachable) + RemoveTracked(item); + + zeroReferred.RemoveWhere(marked.Contains); + } + + private void Mark(StackItem root) + { + if (!marked.Add(root)) return; + pending.Push(root); + while (pending.Count > 0) + { + var current = pending.Pop(); + if (current.ObjectReferences is null) continue; + foreach (var entry in current.ObjectReferences.Values) + { + if (entry.References <= 0) continue; + if (marked.Add(entry.Item)) + pending.Push(entry.Item); + } + } + } + + private void RemoveTracked(StackItem item) + { + trackedItems.Remove(item); + zeroReferred.Remove(item); + item.StackReferences = 0; + + if (item is CompoundType compound) + { + referencesCount -= compound.SubItemsCount; + foreach (var child in compound.SubItems) + { + if (!NeedTrack(child)) continue; + child.ObjectReferences?.Remove(compound); + } + } + + item.ObjectReferences?.Clear(); + item.Cleanup(); + } +} diff --git a/tests/Neo.VM.Tests/UT_ReferenceCounter.cs b/tests/Neo.VM.Tests/UT_ReferenceCounter.cs index dc1e6b05..4b2ea629 100644 --- a/tests/Neo.VM.Tests/UT_ReferenceCounter.cs +++ b/tests/Neo.VM.Tests/UT_ReferenceCounter.cs @@ -13,6 +13,7 @@ using Neo.VM; using Neo.VM.Types; using System; +using System.Collections.Generic; using Array = Neo.VM.Types.Array; namespace Neo.Test; @@ -23,41 +24,10 @@ public class UT_ReferenceCounter [TestMethod] public void TestCircularReferences() { - using ScriptBuilder sb = new(); - sb.Emit(OpCode.INITSSLOT, new byte[] { 1 }); //{}|{null}:1 - sb.EmitPush(0); //{0}|{null}:2 - sb.Emit(OpCode.NEWARRAY); //{A[]}|{null}:2 - sb.Emit(OpCode.DUP); //{A[],A[]}|{null}:3 - sb.Emit(OpCode.DUP); //{A[],A[],A[]}|{null}:4 - sb.Emit(OpCode.APPEND); //{A[A]}|{null}:3 - sb.Emit(OpCode.DUP); //{A[A],A[A]}|{null}:4 - sb.EmitPush(0); //{A[A],A[A],0}|{null}:5 - sb.Emit(OpCode.NEWARRAY); //{A[A],A[A],B[]}|{null}:5 - sb.Emit(OpCode.STSFLD0); //{A[A],A[A]}|{B[]}:4 - sb.Emit(OpCode.LDSFLD0); //{A[A],A[A],B[]}|{B[]}:5 - sb.Emit(OpCode.APPEND); //{A[A,B]}|{B[]}:4 - sb.Emit(OpCode.LDSFLD0); //{A[A,B],B[]}|{B[]}:5 - sb.EmitPush(0); //{A[A,B],B[],0}|{B[]}:6 - sb.Emit(OpCode.NEWARRAY); //{A[A,B],B[],C[]}|{B[]}:6 - sb.Emit(OpCode.TUCK); //{A[A,B],C[],B[],C[]}|{B[]}:7 - sb.Emit(OpCode.APPEND); //{A[A,B],C[]}|{B[C]}:6 - sb.EmitPush(0); //{A[A,B],C[],0}|{B[C]}:7 - sb.Emit(OpCode.NEWARRAY); //{A[A,B],C[],D[]}|{B[C]}:7 - sb.Emit(OpCode.TUCK); //{A[A,B],D[],C[],D[]}|{B[C]}:8 - sb.Emit(OpCode.APPEND); //{A[A,B],D[]}|{B[C[D]]}:7 - sb.Emit(OpCode.LDSFLD0); //{A[A,B],D[],B[C]}|{B[C[D]]}:8 - sb.Emit(OpCode.APPEND); //{A[A,B]}|{B[C[D[B]]]}:7 - sb.Emit(OpCode.PUSHNULL); //{A[A,B],null}|{B[C[D[B]]]}:8 - sb.Emit(OpCode.STSFLD0); //{A[A,B[C[D[B]]]]}|{null}:7 - sb.Emit(OpCode.DUP); //{A[A,B[C[D[B]]]],A[A,B]}|{null}:8 - sb.EmitPush(1); //{A[A,B[C[D[B]]]],A[A,B],1}|{null}:9 - sb.Emit(OpCode.REMOVE); //{A[A]}|{null}:3 - sb.Emit(OpCode.STSFLD0); //{}|{A[A]}:2 - sb.Emit(OpCode.RET); //{}:0 - + byte[] script = BuildCircularReferencesScript(); using ExecutionEngine engine = new(); Debugger debugger = new(engine); - engine.LoadScript(sb.ToArray()); + engine.LoadScript(script); Assert.AreEqual(VMState.BREAK, debugger.StepInto()); Assert.AreEqual(1, engine.ReferenceCounter.Count); Assert.AreEqual(VMState.BREAK, debugger.StepInto()); @@ -123,22 +93,10 @@ public void TestCircularReferences() [TestMethod] public void TestRemoveReferrer() { - using ScriptBuilder sb = new(); - sb.Emit(OpCode.INITSSLOT, new byte[] { 1 }); //{}|{null}:1 - sb.EmitPush(0); //{0}|{null}:2 - sb.Emit(OpCode.NEWARRAY); //{A[]}|{null}:2 - sb.Emit(OpCode.DUP); //{A[],A[]}|{null}:3 - sb.EmitPush(0); //{A[],A[],0}|{null}:4 - sb.Emit(OpCode.NEWARRAY); //{A[],A[],B[]}|{null}:4 - sb.Emit(OpCode.STSFLD0); //{A[],A[]}|{B[]}:3 - sb.Emit(OpCode.LDSFLD0); //{A[],A[],B[]}|{B[]}:4 - sb.Emit(OpCode.APPEND); //{A[B]}|{B[]}:3 - sb.Emit(OpCode.DROP); //{}|{B[]}:1 - sb.Emit(OpCode.RET); //{}:0 - + byte[] script = BuildRemoveReferrerScript(); using ExecutionEngine engine = new(); Debugger debugger = new(engine); - engine.LoadScript(sb.ToArray()); + engine.LoadScript(script); Assert.AreEqual(VMState.BREAK, debugger.StepInto()); Assert.AreEqual(1, engine.ReferenceCounter.Count); Assert.AreEqual(VMState.BREAK, debugger.StepInto()); @@ -257,4 +215,94 @@ public void TestInvalidReferenceStackItem() Assert.ThrowsExactly(() => arr.Add(arr2)); } + + [TestMethod] + public void TestMarkSweepMatchesReferenceCounterOnCircularScript() + { + var script = BuildCircularReferencesScript(); + AssertReferenceCountersProduceSameResult(script, 4); + } + + [TestMethod] + public void TestMarkSweepMatchesReferenceCounterOnRemoveReferrerScript() + { + var script = BuildRemoveReferrerScript(); + AssertReferenceCountersProduceSameResult(script, 1); + } + + private static void AssertReferenceCountersProduceSameResult(byte[] script, int expectedCount) + { + using (ExecutionEngine engine = new()) + { + engine.LoadScript(script); + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.AreEqual(expectedCount, engine.ReferenceCounter.Count); + } + + using CounterEngine markSweepEngine = new(new MarkSweepReferenceCounter()); + markSweepEngine.LoadScript(script); + Assert.AreEqual(VMState.HALT, markSweepEngine.Execute()); + Assert.AreEqual(expectedCount, markSweepEngine.ReferenceCounter.Count); + } + + private static byte[] BuildCircularReferencesScript() + { + using ScriptBuilder sb = new(); + sb.Emit(OpCode.INITSSLOT, new byte[] { 1 }); + sb.EmitPush(0); + sb.Emit(OpCode.NEWARRAY); + sb.Emit(OpCode.DUP); + sb.Emit(OpCode.DUP); + sb.Emit(OpCode.APPEND); + sb.Emit(OpCode.DUP); + sb.EmitPush(0); + sb.Emit(OpCode.NEWARRAY); + sb.Emit(OpCode.STSFLD0); + sb.Emit(OpCode.LDSFLD0); + sb.Emit(OpCode.APPEND); + sb.Emit(OpCode.LDSFLD0); + sb.EmitPush(0); + sb.Emit(OpCode.NEWARRAY); + sb.Emit(OpCode.TUCK); + sb.Emit(OpCode.APPEND); + sb.EmitPush(0); + sb.Emit(OpCode.NEWARRAY); + sb.Emit(OpCode.TUCK); + sb.Emit(OpCode.APPEND); + sb.Emit(OpCode.LDSFLD0); + sb.Emit(OpCode.APPEND); + sb.Emit(OpCode.PUSHNULL); + sb.Emit(OpCode.STSFLD0); + sb.Emit(OpCode.DUP); + sb.EmitPush(1); + sb.Emit(OpCode.REMOVE); + sb.Emit(OpCode.STSFLD0); + sb.Emit(OpCode.RET); + return sb.ToArray(); + } + + private static byte[] BuildRemoveReferrerScript() + { + using ScriptBuilder sb = new(); + sb.Emit(OpCode.INITSSLOT, new byte[] { 1 }); + sb.EmitPush(0); + sb.Emit(OpCode.NEWARRAY); + sb.Emit(OpCode.DUP); + sb.EmitPush(0); + sb.Emit(OpCode.NEWARRAY); + sb.Emit(OpCode.STSFLD0); + sb.Emit(OpCode.LDSFLD0); + sb.Emit(OpCode.APPEND); + sb.Emit(OpCode.DROP); + sb.Emit(OpCode.RET); + return sb.ToArray(); + } + + private sealed class CounterEngine : ExecutionEngine + { + public CounterEngine(IReferenceCounter counter) + : base(null, counter, ExecutionEngineLimits.Default) + { + } + } }