Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions benchmarks/Neo.VM.Benchmarks/ReferenceCounterBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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<Array> 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))
};
}
2 changes: 1 addition & 1 deletion src/Neo.VM/ExecutionEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ protected internal set
/// </summary>
/// <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.


/// <summary>
/// Initializes a new instance of the <see cref="ExecutionEngine"/> class
Expand Down
171 changes: 171 additions & 0 deletions src/Neo.VM/MarkSweepReferenceCounter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Reference counter that performs a deterministic mark-sweep collection.
/// </summary>
public sealed class MarkSweepReferenceCounter : IReferenceCounter
{
private readonly HashSet<StackItem> trackedItems = new(ReferenceEqualityComparer.Instance);
private readonly HashSet<StackItem> zeroReferred = new(ReferenceEqualityComparer.Instance);
private readonly Stack<StackItem> pending = new();
private readonly HashSet<StackItem> marked = new(ReferenceEqualityComparer.Instance);
private readonly List<StackItem> unreachable = new();

private int referencesCount;

/// <inheritdoc/>
public int Count => referencesCount;

/// <inheritdoc/>
public void AddZeroReferred(StackItem item)
{
if (zeroReferred.Add(item) && NeedTrack(item))
trackedItems.Add(item);
}

/// <inheritdoc/>
public void AddReference(StackItem item, CompoundType parent)
{
referencesCount++;
if (!NeedTrack(item)) return;

Track(item);

item.ObjectReferences ??= new Dictionary<CompoundType, StackItem.ObjectReferenceEntry>(ReferenceEqualityComparer.Instance);
if (!item.ObjectReferences.TryGetValue(parent, out var entry))
{
entry = new StackItem.ObjectReferenceEntry(parent);
item.ObjectReferences.Add(parent, entry);
}
entry.References++;
}

/// <inheritdoc/>
public void AddStackReference(StackItem item, int count = 1)
{
referencesCount += count;
if (!NeedTrack(item)) return;

Track(item);
item.StackReferences += count;
zeroReferred.Remove(item);
}

/// <inheritdoc/>
public int CheckZeroReferred()
{
if (zeroReferred.Count == 0 || trackedItems.Count == 0)
return referencesCount;

Collect();
return referencesCount;
}

/// <inheritdoc/>
public void RemoveReference(StackItem item, CompoundType parent)
{
referencesCount--;
if (!NeedTrack(item)) return;

item.ObjectReferences![parent].References--;
if (item.StackReferences == 0)
zeroReferred.Add(item);
}

/// <inheritdoc/>
public void RemoveStackReference(StackItem item)
{
referencesCount--;
if (!NeedTrack(item)) return;

if (--item.StackReferences == 0)
zeroReferred.Add(item);
}

/// <summary>
/// Only compound types and buffers require tracking because they own other items or pooled memory.
/// </summary>
[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()
{
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.
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;

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?

foreach (var child in compound.SubItems)
{
if (!NeedTrack(child)) continue;
child.ObjectReferences?.Remove(compound);
}
}

item.ObjectReferences?.Clear();
item.Cleanup();
}
}
Loading