diff --git a/csharp/Platform.Data/Sequences/EmitOptimizedSequenceWalker.cs b/csharp/Platform.Data/Sequences/EmitOptimizedSequenceWalker.cs new file mode 100644 index 0000000..14c9e18 --- /dev/null +++ b/csharp/Platform.Data/Sequences/EmitOptimizedSequenceWalker.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Platform.Data.Sequences +{ + public static class EmitOptimizedSequenceWalker + { + private static readonly ConcurrentDictionary _compiledMethods = new(); + + // Global stack per thread to avoid allocation overhead + [ThreadStatic] + private static Stack? _globalStack; + + private static Stack GetGlobalStack() + { + return _globalStack ??= new Stack(256); // Pre-allocate for better performance + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WalkRight(TLink sequence, Func getSource, Func getTarget, + Func isElement, Action visit) + { + var compiled = GetOrCreateCompiledWalker(WalkerDirection.Right); + compiled(sequence, getSource, getTarget, isElement, visit); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WalkLeft(TLink sequence, Func getSource, Func getTarget, + Func isElement, Action visit) + { + var compiled = GetOrCreateCompiledWalker(WalkerDirection.Left); + compiled(sequence, getSource, getTarget, isElement, visit); + } + + private static Action, Func, Func, Action> + GetOrCreateCompiledWalker(WalkerDirection direction) + { + var key = $"{typeof(TLink).FullName}_{direction}"; + + return (Action, Func, Func, Action>) + _compiledMethods.GetOrAdd(key, _ => CreateCompiledWalker(direction)); + } + + private static Delegate CreateCompiledWalker(WalkerDirection direction) + { + var method = new DynamicMethod( + $"CompiledWalk{direction}_{typeof(TLink).Name}", + typeof(void), + new[] { typeof(TLink), typeof(Func), typeof(Func), typeof(Func), typeof(Action) }, + typeof(EmitOptimizedSequenceWalker), + true); + + var il = method.GetILGenerator(); + + // Local variables + var stackLocal = il.DeclareLocal(typeof(Stack)); // stack + var currentLocal = il.DeclareLocal(typeof(object)); // current + var sourceLocal = il.DeclareLocal(typeof(TLink)); // source + var targetLocal = il.DeclareLocal(typeof(TLink)); // target + var typedCurrentLocal = il.DeclareLocal(typeof(TLink)); // typed current + + // Labels + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + var isElementLabel = il.DefineLabel(); + var notElementLabel = il.DefineLabel(); + var checkTarget = il.DefineLabel(); + var checkSource = il.DefineLabel(); + var skipTarget = il.DefineLabel(); + var skipSource = il.DefineLabel(); + + // Get global stack + il.EmitCall(OpCodes.Call, typeof(EmitOptimizedSequenceWalker).GetMethod(nameof(GetGlobalStack), BindingFlags.NonPublic | BindingFlags.Static)!, null); + il.Emit(OpCodes.Stloc, stackLocal); + + // Clear the stack (in case it has leftover items) + il.Emit(OpCodes.Ldloc, stackLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Stack).GetMethod("Clear")!, null); + + // Push initial sequence (boxed) + il.Emit(OpCodes.Ldloc, stackLocal); + il.Emit(OpCodes.Ldarg_0); // sequence + il.Emit(OpCodes.Box, typeof(TLink)); + il.EmitCall(OpCodes.Callvirt, typeof(Stack).GetMethod("Push")!, null); + + // Main loop + il.MarkLabel(loopStart); + + // Check if stack is empty + il.Emit(OpCodes.Ldloc, stackLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Stack).GetProperty("Count")!.GetGetMethod()!, null); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Beq, loopEnd); + + // Pop current + il.Emit(OpCodes.Ldloc, stackLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Stack).GetMethod("Pop")!, null); + il.Emit(OpCodes.Stloc, currentLocal); + + // Unbox current + il.Emit(OpCodes.Ldloc, currentLocal); + il.Emit(OpCodes.Unbox_Any, typeof(TLink)); + il.Emit(OpCodes.Stloc, typedCurrentLocal); + + // Check if element + il.Emit(OpCodes.Ldarg, 3); // isElement + il.Emit(OpCodes.Ldloc, typedCurrentLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Func).GetMethod("Invoke")!, null); + il.Emit(OpCodes.Brtrue, isElementLabel); + + // Not an element - get source and target + il.MarkLabel(notElementLabel); + + // Get source + il.Emit(OpCodes.Ldarg_1); // getSource + il.Emit(OpCodes.Ldloc, typedCurrentLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Func).GetMethod("Invoke")!, null); + il.Emit(OpCodes.Stloc, sourceLocal); + + // Get target + il.Emit(OpCodes.Ldarg_2); // getTarget + il.Emit(OpCodes.Ldloc, typedCurrentLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Func).GetMethod("Invoke")!, null); + il.Emit(OpCodes.Stloc, targetLocal); + + // Push elements based on direction + if (direction == WalkerDirection.Right) + { + // Push target first (for right-to-left traversal) + il.Emit(OpCodes.Br, checkTarget); + + il.MarkLabel(checkTarget); + EmitPushIfNotDefault(il, stackLocal, targetLocal, typeof(TLink)); + + il.Emit(OpCodes.Br, checkSource); + + il.MarkLabel(checkSource); + EmitPushIfNotDefault(il, stackLocal, sourceLocal, typeof(TLink)); + } + else + { + // Push source first (for left-to-right traversal) + il.Emit(OpCodes.Br, checkSource); + + il.MarkLabel(checkSource); + EmitPushIfNotDefault(il, stackLocal, sourceLocal, typeof(TLink)); + + il.Emit(OpCodes.Br, checkTarget); + + il.MarkLabel(checkTarget); + EmitPushIfNotDefault(il, stackLocal, targetLocal, typeof(TLink)); + } + + il.Emit(OpCodes.Br, loopStart); + + // Is element - visit it + il.MarkLabel(isElementLabel); + il.Emit(OpCodes.Ldarg, 4); // visit + il.Emit(OpCodes.Ldloc, typedCurrentLocal); + il.EmitCall(OpCodes.Callvirt, typeof(Action).GetMethod("Invoke")!, null); + il.Emit(OpCodes.Br, loopStart); + + // End of method + il.MarkLabel(loopEnd); + il.Emit(OpCodes.Ret); + + return method.CreateDelegate(typeof(Action, Func, Func, Action>)); + } + + private static void EmitPushIfNotDefault(ILGenerator il, LocalBuilder stackLocal, LocalBuilder valueLocal, Type linkType) + { + var skipLabel = il.DefineLabel(); + + // Load value and default for comparison + il.Emit(OpCodes.Ldloc, valueLocal); + + // For reference types, compare with null + if (!linkType.IsValueType) + { + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Beq, skipLabel); + } + else + { + // For value types, use default comparison + var defaultLocal = il.DeclareLocal(linkType); + il.Emit(OpCodes.Ldloca, defaultLocal); + il.Emit(OpCodes.Initobj, linkType); + il.Emit(OpCodes.Ldloc, defaultLocal); + + // Use EqualityComparer.Default.Equals for value type comparison + var comparerType = typeof(EqualityComparer<>).MakeGenericType(linkType); + il.EmitCall(OpCodes.Call, comparerType.GetProperty("Default")!.GetGetMethod()!, null); + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Ldloc, defaultLocal); + il.EmitCall(OpCodes.Callvirt, comparerType.GetMethod("Equals", new[] { linkType, linkType })!, null); + il.Emit(OpCodes.Brtrue, skipLabel); + } + + // Push value (boxed) onto stack + il.Emit(OpCodes.Ldloc, stackLocal); + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Box, linkType); + il.EmitCall(OpCodes.Callvirt, typeof(Stack).GetMethod("Push")!, null); + + il.MarkLabel(skipLabel); + } + + private enum WalkerDirection + { + Left, + Right + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data/Sequences/ISequenceWalker.cs b/csharp/Platform.Data/Sequences/ISequenceWalker.cs new file mode 100644 index 0000000..e5fca83 --- /dev/null +++ b/csharp/Platform.Data/Sequences/ISequenceWalker.cs @@ -0,0 +1,10 @@ +using System; + +namespace Platform.Data.Sequences +{ + public interface ISequenceWalker + { + void WalkRight(T sequence, Func getSource, Func getTarget, Func isElement, Action visit); + void WalkLeft(T sequence, Func getSource, Func getTarget, Func isElement, Action visit); + } +} \ No newline at end of file diff --git a/csharp/Platform.Data/Sequences/SequenceWalker.cs b/csharp/Platform.Data/Sequences/SequenceWalker.cs new file mode 100644 index 0000000..b0b15ed --- /dev/null +++ b/csharp/Platform.Data/Sequences/SequenceWalker.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Platform.Data.Sequences +{ + public static class SequenceWalker + { + // TODO: Can use global stack (or multiple global stacks per thread) + // TODO: Recursion depth limit might allow reduced stack size + // TODO: Try to implement the algorithms using System.Reflection.Emit and a low-level stack + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WalkRight(TLink sequence, Func getSource, Func getTarget, Func isElement, Action visit) + { + var stack = new Stack(); + stack.Push(sequence); + + while (stack.Count > 0) + { + var current = stack.Pop(); + + if (isElement(current)) + { + visit(current); + } + else + { + var source = getSource(current); + var target = getTarget(current); + + // Push in reverse order for right-to-left traversal + if (!Equals(target, default(TLink))) + stack.Push(target); + if (!Equals(source, default(TLink))) + stack.Push(source); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WalkLeft(TLink sequence, Func getSource, Func getTarget, Func isElement, Action visit) + { + var stack = new Stack(); + stack.Push(sequence); + + while (stack.Count > 0) + { + var current = stack.Pop(); + + if (isElement(current)) + { + visit(current); + } + else + { + var source = getSource(current); + var target = getTarget(current); + + // Push in order for left-to-right traversal + if (!Equals(source, default(TLink))) + stack.Push(source); + if (!Equals(target, default(TLink))) + stack.Push(target); + } + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data/Sequences/StopableSequenceWalker.cs b/csharp/Platform.Data/Sequences/StopableSequenceWalker.cs new file mode 100644 index 0000000..4b1b8c3 --- /dev/null +++ b/csharp/Platform.Data/Sequences/StopableSequenceWalker.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Platform.Data.Sequences +{ + public static class StopableSequenceWalker + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool WalkRight(TLink sequence, Func getSource, Func getTarget, Func isElement, Action visit) + { + return WalkRight(sequence, getSource, getTarget, isElement, _ => { }, _ => { }, _ => true, element => { visit(element); return true; }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool WalkRight(TLink sequence, Func getSource, Func getTarget, Func isElement, + Action enter, Action exit, Func canEnter, Func visit) + { + var stack = new Stack<(TLink element, bool processed)>(); + stack.Push((sequence, false)); + + while (stack.Count > 0) + { + var (current, processed) = stack.Pop(); + + if (processed) + { + exit(current); + continue; + } + + if (!canEnter(current)) + continue; + + enter(current); + + if (isElement(current)) + { + if (!visit(current)) + return false; + } + else + { + // Push exit marker + stack.Push((current, true)); + + var source = getSource(current); + var target = getTarget(current); + + // Push in reverse order for right-to-left traversal + if (!Equals(target, default(TLink))) + stack.Push((target, false)); + if (!Equals(source, default(TLink))) + stack.Push((source, false)); + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool WalkLeft(TLink sequence, Func getSource, Func getTarget, Func isElement, + Action enter, Action exit, Func canEnter, Func visit) + { + var stack = new Stack<(TLink element, bool processed)>(); + stack.Push((sequence, false)); + + while (stack.Count > 0) + { + var (current, processed) = stack.Pop(); + + if (processed) + { + exit(current); + continue; + } + + if (!canEnter(current)) + continue; + + enter(current); + + if (isElement(current)) + { + if (!visit(current)) + return false; + } + else + { + // Push exit marker + stack.Push((current, true)); + + var source = getSource(current); + var target = getTarget(current); + + // Push in order for left-to-right traversal + if (!Equals(source, default(TLink))) + stack.Push((source, false)); + if (!Equals(target, default(TLink))) + stack.Push((target, false)); + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/experiments/SequenceWalkerBenchmark.cs b/experiments/SequenceWalkerBenchmark.cs new file mode 100644 index 0000000..0d2a0eb --- /dev/null +++ b/experiments/SequenceWalkerBenchmark.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Platform.Data.Sequences; + +namespace Platform.Data.Experiments +{ + public class SequenceWalkerBenchmark + { + // Test data structure - simple binary tree node + public class TestNode + { + public int Value { get; set; } + public TestNode? Left { get; set; } + public TestNode? Right { get; set; } + + public TestNode(int value) { Value = value; } + } + + // Create a test binary tree + private static TestNode CreateTestTree(int depth, int startValue = 1) + { + if (depth == 0) return new TestNode(startValue); + + var node = new TestNode(startValue); + node.Left = CreateTestTree(depth - 1, startValue * 2); + node.Right = CreateTestTree(depth - 1, startValue * 2 + 1); + return node; + } + + // Create a large sequence (linked list structure) + private static TestNode CreateTestSequence(int length) + { + var head = new TestNode(1); + var current = head; + + for (int i = 2; i <= length; i++) + { + current.Right = new TestNode(i); + current = current.Right; + } + + return head; + } + + public static void RunBenchmarks() + { + Console.WriteLine("=== SequenceWalker Performance Benchmarks ===\n"); + + // Test cases + var testCases = new[] + { + ("Small Tree (depth 5)", CreateTestTree(5), 1000), + ("Medium Tree (depth 10)", CreateTestTree(10), 100), + ("Large Tree (depth 15)", CreateTestTree(15), 10), + ("Small Sequence (100 nodes)", CreateTestSequence(100), 1000), + ("Medium Sequence (1000 nodes)", CreateTestSequence(1000), 100), + ("Large Sequence (10000 nodes)", CreateTestSequence(10000), 10) + }; + + foreach (var (name, testData, iterations) in testCases) + { + Console.WriteLine($"Testing: {name} ({iterations} iterations)"); + RunSingleBenchmark(testData, iterations); + Console.WriteLine(); + } + } + + private static void RunSingleBenchmark(TestNode root, int iterations) + { + var results = new List(); + + // Helper functions + Func getLeft = node => node?.Left; + Func getRight = node => node?.Right; + Func isLeaf = node => node != null && node.Left == null && node.Right == null; + Action collect = node => { if (node != null) results.Add(node.Value); }; + + // Warm up JIT + for (int i = 0; i < 5; i++) + { + results.Clear(); + SequenceWalker.WalkRight(root, getLeft, getRight, isLeaf, collect); + results.Clear(); + EmitOptimizedSequenceWalker.WalkRight(root, getLeft, getRight, isLeaf, collect); + } + + // Benchmark Original SequenceWalker + results.Clear(); + var sw = Stopwatch.StartNew(); + + for (int i = 0; i < iterations; i++) + { + results.Clear(); + SequenceWalker.WalkRight(root, getLeft, getRight, isLeaf, collect); + } + + sw.Stop(); + var originalTime = sw.Elapsed; + var originalNodesProcessed = results.Count; + + // Benchmark EmitOptimizedSequenceWalker + results.Clear(); + sw = Stopwatch.StartNew(); + + for (int i = 0; i < iterations; i++) + { + results.Clear(); + EmitOptimizedSequenceWalker.WalkRight(root, getLeft, getRight, isLeaf, collect); + } + + sw.Stop(); + var optimizedTime = sw.Elapsed; + var optimizedNodesProcessed = results.Count; + + // Verify results are identical + var resultsMatch = originalNodesProcessed == optimizedNodesProcessed; + + // Calculate performance metrics + var improvementRatio = originalTime.TotalMilliseconds / optimizedTime.TotalMilliseconds; + var improvementPercent = ((originalTime.TotalMilliseconds - optimizedTime.TotalMilliseconds) / originalTime.TotalMilliseconds) * 100; + + // Display results + Console.WriteLine($" Original SequenceWalker: {originalTime.TotalMilliseconds:F2} ms ({originalNodesProcessed} nodes)"); + Console.WriteLine($" EmitOptimized SequenceWalker: {optimizedTime.TotalMilliseconds:F2} ms ({optimizedNodesProcessed} nodes)"); + Console.WriteLine($" Results match: {resultsMatch}"); + Console.WriteLine($" Performance improvement: {improvementRatio:F2}x ({improvementPercent:F1}%)"); + + if (improvementRatio > 1) + Console.WriteLine($" ✓ EmitOptimized is {improvementRatio:F2}x faster"); + else if (improvementRatio < 0.9) + Console.WriteLine($" ⚠ EmitOptimized is {1/improvementRatio:F2}x slower"); + else + Console.WriteLine($" ≈ Performance is similar"); + } + + // Memory usage benchmark + public static void RunMemoryBenchmark() + { + Console.WriteLine("=== Memory Usage Benchmark ===\n"); + + var testData = CreateTestTree(12); + const int iterations = 1000; + + // Force GC before measurement + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var initialMemory = GC.GetTotalMemory(false); + + // Test original implementation + var results = new List(); + Func getLeft = node => node?.Left; + Func getRight = node => node?.Right; + Func isLeaf = node => node != null && node.Left == null && node.Right == null; + Action collect = node => { if (node != null) results.Add(node.Value); }; + + for (int i = 0; i < iterations; i++) + { + results.Clear(); + SequenceWalker.WalkRight(testData, getLeft, getRight, isLeaf, collect); + } + + var memoryAfterOriginal = GC.GetTotalMemory(false); + + // Force GC + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // Test optimized implementation + for (int i = 0; i < iterations; i++) + { + results.Clear(); + EmitOptimizedSequenceWalker.WalkRight(testData, getLeft, getRight, isLeaf, collect); + } + + var memoryAfterOptimized = GC.GetTotalMemory(false); + + Console.WriteLine($"Initial memory: {initialMemory:N0} bytes"); + Console.WriteLine($"After original: {memoryAfterOriginal:N0} bytes (diff: {memoryAfterOriginal - initialMemory:N0})"); + Console.WriteLine($"After optimized: {memoryAfterOptimized:N0} bytes (diff: {memoryAfterOptimized - memoryAfterOriginal:N0})"); + } + } + + // Simple test program + class Program + { + static void Main(string[] args) + { + try + { + SequenceWalkerBenchmark.RunBenchmarks(); + Console.WriteLine(); + SequenceWalkerBenchmark.RunMemoryBenchmark(); + + Console.WriteLine("\nPress any key to exit..."); + Console.ReadKey(); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + } +} \ No newline at end of file diff --git a/experiments/SequenceWalkerBenchmark.csproj b/experiments/SequenceWalkerBenchmark.csproj new file mode 100644 index 0000000..cc0218d --- /dev/null +++ b/experiments/SequenceWalkerBenchmark.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8 + Platform.Data.Experiments + true + + + + + + + \ No newline at end of file