SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+893
View File
@@ -0,0 +1,893 @@
namespace QuanTAlib.Tests;
public class RingBufferTests
{
[Fact]
public void Constructor_ValidCapacity_CreatesBuffer()
{
var buffer = new RingBuffer(10);
Assert.Equal(10, buffer.Capacity);
Assert.Equal(0, buffer.Count);
Assert.False(buffer.IsFull);
Assert.Equal(0, buffer.Sum);
Assert.Equal(0, buffer.Average);
}
[Fact]
public void Constructor_ZeroCapacity_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new RingBuffer(0));
}
[Fact]
public void Constructor_NegativeCapacity_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new RingBuffer(-1));
}
[Fact]
public void Add_SingleValue_UpdatesState()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
Assert.Equal(1, buffer.Count);
Assert.Equal(10.0, buffer.Sum);
Assert.Equal(10.0, buffer.Average);
Assert.Equal(10.0, buffer.Newest);
Assert.Equal(10.0, buffer.Oldest);
Assert.False(buffer.IsFull);
}
[Fact]
public void Add_MultipleValues_UpdatesState()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
Assert.Equal(3, buffer.Count);
Assert.Equal(60.0, buffer.Sum);
Assert.Equal(20.0, buffer.Average);
Assert.Equal(30.0, buffer.Newest);
Assert.Equal(10.0, buffer.Oldest);
Assert.False(buffer.IsFull);
}
[Fact]
public void Add_FillBuffer_BecomesFullAndWraps()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
Assert.Equal(3, buffer.Count);
Assert.True(buffer.IsFull);
Assert.Equal(60.0, buffer.Sum);
Assert.Equal(20.0, buffer.Average);
// Add one more - should remove 10.0
double removed = buffer.Add(40.0);
Assert.Equal(10.0, removed);
Assert.Equal(3, buffer.Count);
Assert.True(buffer.IsFull);
Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
Assert.Equal(30.0, buffer.Average);
Assert.Equal(40.0, buffer.Newest);
Assert.Equal(20.0, buffer.Oldest);
}
[Fact]
public void Add_MultipleWraps_MaintainsCorrectState()
{
var buffer = new RingBuffer(3);
// Fill and wrap multiple times
for (int i = 1; i <= 10; i++)
{
buffer.Add(i * 10.0);
}
// Should contain: 80, 90, 100
Assert.Equal(3, buffer.Count);
Assert.True(buffer.IsFull);
Assert.Equal(270.0, buffer.Sum); // 80 + 90 + 100
Assert.Equal(90.0, buffer.Average);
Assert.Equal(100.0, buffer.Newest);
Assert.Equal(80.0, buffer.Oldest);
}
[Fact]
public void UpdateNewest_ModifiesLastValue()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
Assert.Equal(60.0, buffer.Sum);
buffer.UpdateNewest(35.0);
Assert.Equal(65.0, buffer.Sum); // 10 + 20 + 35
Assert.Equal(35.0, buffer.Newest);
Assert.Equal(3, buffer.Count);
}
[Fact]
public void UpdateNewest_EmptyBuffer_DoesNothing()
{
var buffer = new RingBuffer(5);
buffer.UpdateNewest(100.0); // Should not throw
Assert.Equal(0, buffer.Count);
Assert.Equal(0, buffer.Sum);
}
[Fact]
public void Indexer_AccessesCorrectValues()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
// Index 0 = oldest, Index 2 = newest
Assert.Equal(10.0, buffer[0]);
Assert.Equal(20.0, buffer[1]);
Assert.Equal(30.0, buffer[2]);
}
[Fact]
public void Indexer_AfterWrap_AccessesCorrectValues()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0); // Wraps, removes 10
// Should contain: 20, 30, 40
Assert.Equal(20.0, buffer[0]);
Assert.Equal(30.0, buffer[1]);
Assert.Equal(40.0, buffer[2]);
}
[Fact]
public void Indexer_OutOfRange_ThrowsException()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
// Valid indices are 0 and 1 (2 elements)
// Index 2 should throw ArgumentOutOfRangeException
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[2]);
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[10]);
}
[Fact]
public void Clear_ResetsState()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Clear();
Assert.Equal(0, buffer.Count);
Assert.Equal(0, buffer.Sum);
Assert.Equal(0, buffer.Average);
Assert.False(buffer.IsFull);
}
[Fact]
public void Clear_AllowsReuse()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Clear();
buffer.Add(100.0);
Assert.Equal(1, buffer.Count);
Assert.Equal(100.0, buffer.Sum);
Assert.Equal(100.0, buffer.Newest);
}
[Fact]
public void Clone_CreatesIndependentCopy()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
var clone = buffer.Clone();
// Verify clone has same state
Assert.Equal(buffer.Count, clone.Count);
Assert.Equal(buffer.Sum, clone.Sum);
Assert.Equal(buffer.Newest, clone.Newest);
Assert.Equal(buffer.Oldest, clone.Oldest);
// Modify original - clone should be unaffected
buffer.Add(40.0);
Assert.Equal(4, buffer.Count);
Assert.Equal(3, clone.Count);
Assert.Equal(100.0, buffer.Sum);
Assert.Equal(60.0, clone.Sum);
}
[Fact]
public void CopyFrom_CopiesState()
{
var source = new RingBuffer(5);
var target = new RingBuffer(5);
source.Add(10.0);
source.Add(20.0);
source.Add(30.0);
target.Add(100.0); // Different initial state
target.CopyFrom(source);
Assert.Equal(source.Count, target.Count);
Assert.Equal(source.Sum, target.Sum);
Assert.Equal(source.Newest, target.Newest);
Assert.Equal(source.Oldest, target.Oldest);
// Verify independence after copy
source.Add(40.0);
Assert.NotEqual(source.Sum, target.Sum);
}
[Fact]
public void CopyFrom_DifferentCapacity_ThrowsException()
{
var source = new RingBuffer(5);
var target = new RingBuffer(10);
Assert.Throws<ArgumentException>(() => target.CopyFrom(source));
}
[Fact]
public void GetSpan_ReturnsChronologicalOrder()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
var span = buffer.GetSpan();
Assert.Equal(3, span.Length);
Assert.Equal(10.0, span[0]);
Assert.Equal(20.0, span[1]);
Assert.Equal(30.0, span[2]);
}
[Fact]
public void GetSpan_AfterWrap_ReturnsChronologicalOrder()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0);
buffer.Add(50.0);
var span = buffer.GetSpan();
// Should be: 30, 40, 50
Assert.Equal(3, span.Length);
Assert.Equal(30.0, span[0]);
Assert.Equal(40.0, span[1]);
Assert.Equal(50.0, span[2]);
}
[Fact]
public void GetSpan_EmptyBuffer_ReturnsEmpty()
{
var buffer = new RingBuffer(5);
var span = buffer.GetSpan();
Assert.True(span.IsEmpty);
}
[Fact]
public void Min_ReturnsMinimumValue()
{
var buffer = new RingBuffer(5);
buffer.Add(30.0);
buffer.Add(10.0);
buffer.Add(50.0);
buffer.Add(20.0);
buffer.Add(40.0);
Assert.Equal(10.0, buffer.Min());
}
[Fact]
public void Max_ReturnsMaximumValue()
{
var buffer = new RingBuffer(5);
buffer.Add(30.0);
buffer.Add(10.0);
buffer.Add(50.0);
buffer.Add(20.0);
buffer.Add(40.0);
Assert.Equal(50.0, buffer.Max());
}
[Fact]
public void Min_EmptyBuffer_ReturnsNaN()
{
var buffer = new RingBuffer(5);
Assert.True(double.IsNaN(buffer.Min()));
}
[Fact]
public void Max_EmptyBuffer_ReturnsNaN()
{
var buffer = new RingBuffer(5);
Assert.True(double.IsNaN(buffer.Max()));
}
[Fact]
public void Enumerator_IteratesInChronologicalOrder()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
List<double> values = [];
foreach (var v in buffer)
{
values.Add(v);
}
Assert.Equal(3, values.Count);
Assert.Equal(10.0, values[0]);
Assert.Equal(20.0, values[1]);
Assert.Equal(30.0, values[2]);
}
[Fact]
public void Enumerator_AfterWrap_IteratesInChronologicalOrder()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0);
buffer.Add(50.0);
List<double> values = [];
foreach (var v in buffer)
{
values.Add(v);
}
// Should be: 30, 40, 50
Assert.Equal(3, values.Count);
Assert.Equal(30.0, values[0]);
Assert.Equal(40.0, values[1]);
Assert.Equal(50.0, values[2]);
}
[Fact]
public void Add_WithIsNew_WorksCorrectly()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0, isNew: true);
buffer.Add(20.0, isNew: true);
buffer.Add(25.0, isNew: false); // Should update 20.0 to 25.0
Assert.Equal(2, buffer.Count);
Assert.Equal(25.0, buffer.Newest);
Assert.Equal(35.0, buffer.Sum); // 10 + 25
}
[Fact]
public void Indexer_WithIndexType_SupportsFromEnd()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
Assert.Equal(30.0, buffer[^1]); // Newest
Assert.Equal(20.0, buffer[^2]);
Assert.Equal(10.0, buffer[^3]); // Oldest
}
[Fact]
public void Newest_EmptyBuffer_ReturnsNaN()
{
var buffer = new RingBuffer(5);
Assert.True(double.IsNaN(buffer.Newest));
}
[Fact]
public void Oldest_EmptyBuffer_ReturnsNaN()
{
var buffer = new RingBuffer(5);
Assert.True(double.IsNaN(buffer.Oldest));
}
[Fact]
public void Average_EmptyBuffer_ReturnsZero()
{
var buffer = new RingBuffer(5);
Assert.Equal(0, buffer.Average);
}
[Fact]
public void GetInternalSpan_ReturnsFullBuffer()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
var span = buffer.GetInternalSpan();
Assert.Equal(5, span.Length); // Full capacity, not count
}
[Fact]
public void ToArray_AfterWrap_ReturnsChronologicalOrder()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0); // Wraps
var arr = buffer.ToArray();
Assert.Equal(3, arr.Length);
Assert.Equal(20.0, arr[0]);
Assert.Equal(30.0, arr[1]);
Assert.Equal(40.0, arr[2]);
}
[Fact]
public void CopyTo_AfterWrap_CopiesInChronologicalOrder()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0); // Wraps
var dest = new double[5];
buffer.CopyTo(dest, 1);
Assert.Equal(0, dest[0]); // Untouched
Assert.Equal(20.0, dest[1]);
Assert.Equal(30.0, dest[2]);
Assert.Equal(40.0, dest[3]);
Assert.Equal(0, dest[4]); // Untouched
}
[Fact]
public void Indexer_Set_UpdatesValueAndSum()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
Assert.Equal(60.0, buffer.Sum);
buffer[1] = 25.0; // Change 20.0 to 25.0
Assert.Equal(65.0, buffer.Sum);
Assert.Equal(25.0, buffer[1]);
}
[Fact]
public void Indexer_SetFromEnd_UpdatesValueAndSum()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer[^1] = 35.0; // Change newest (30.0) to 35.0
Assert.Equal(65.0, buffer.Sum);
Assert.Equal(35.0, buffer[^1]);
}
[Fact]
public void Min_LargeBuffer_UsesSimd()
{
var buffer = new RingBuffer(100);
for (int i = 0; i < 100; i++)
{
buffer.Add(i + 1); // 1 to 100
}
Assert.Equal(1.0, buffer.Min());
}
[Fact]
public void Max_LargeBuffer_UsesSimd()
{
var buffer = new RingBuffer(100);
for (int i = 0; i < 100; i++)
{
buffer.Add(i + 1); // 1 to 100
}
Assert.Equal(100.0, buffer.Max());
}
[Fact]
public void ToArray_EmptyBuffer_ReturnsEmpty()
{
var buffer = new RingBuffer(5);
var arr = buffer.ToArray();
Assert.Empty(arr);
}
[Fact]
public void CopyTo_EmptyBuffer_DoesNothing()
{
var buffer = new RingBuffer(5);
double[] dest = [1.0, 2.0, 3.0];
buffer.CopyTo(dest, 0);
Assert.Equal(1.0, dest[0]);
Assert.Equal(2.0, dest[1]);
Assert.Equal(3.0, dest[2]);
}
[Fact]
public void InternalBuffer_ReturnsSpan()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
var span = buffer.InternalBuffer;
Assert.Equal(5, span.Length);
Assert.Equal(10.0, span[0]);
}
[Fact]
public void Enumerator_Reset_AllowsReIteration()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
var enumerator = buffer.GetEnumerator();
// First iteration
Assert.True(enumerator.MoveNext());
Assert.Equal(10.0, enumerator.Current);
Assert.True(enumerator.MoveNext());
Assert.Equal(20.0, enumerator.Current);
Assert.False(enumerator.MoveNext());
// Reset and iterate again
enumerator.Reset();
Assert.True(enumerator.MoveNext());
Assert.Equal(10.0, enumerator.Current);
enumerator.Dispose(); // Coverage for Dispose
}
[Fact]
public void IEnumerable_GetEnumerator_Works()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
IEnumerable<double> enumerable = buffer;
List<double> values = [];
foreach (var v in enumerable)
{
values.Add(v);
}
Assert.Equal(2, values.Count);
Assert.Equal(10.0, values[0]);
Assert.Equal(20.0, values[1]);
}
[Fact]
public void IEnumerable_NonGeneric_GetEnumerator_Works()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
IEnumerable enumerable = buffer;
List<double> values = [];
foreach (var v in enumerable)
{
values.Add((double)v);
}
Assert.Equal(2, values.Count);
}
[Fact]
public void Indexer_Set_AfterWrap_UpdatesCorrectly()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0); // Wraps - now has 20, 30, 40
buffer[0] = 25.0; // Change oldest (20.0) to 25.0
Assert.Equal(95.0, buffer.Sum); // 25 + 30 + 40
Assert.Equal(25.0, buffer[0]);
}
[Fact]
public void Add_WithIsNew_EmptyBuffer_AddsValue()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0, isNew: false); // isNew=false but buffer empty, should still add
Assert.Equal(1, buffer.Count);
Assert.Equal(10.0, buffer.Newest);
}
[Fact]
public void BarCorrection_Workflow()
{
// Simulate bar correction (isNew=false) workflow
var buffer = new RingBuffer(3);
var backup = new RingBuffer(3);
// Add values as new bars
buffer.Add(10.0);
backup.CopyFrom(buffer);
buffer.Add(20.0);
backup.CopyFrom(buffer);
buffer.Add(30.0);
backup.CopyFrom(buffer);
double avgBeforeCorrection = buffer.Average;
// Simulate correction (isNew=false)
buffer.CopyFrom(backup); // Restore previous state
buffer.UpdateNewest(35.0); // Update with corrected value
// Average should reflect the correction
Assert.Equal(21.666666666666668, buffer.Average, 1e-10);
Assert.NotEqual(avgBeforeCorrection, buffer.Average);
}
[Fact]
public void Snapshot_CapturesCurrentState()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Snapshot();
// Modify buffer after snapshot
buffer.Add(40.0);
Assert.Equal(4, buffer.Count);
Assert.Equal(100.0, buffer.Sum); // 10 + 20 + 30 + 40
}
[Fact]
public void Restore_ReturnsToSnapshotState()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Snapshot();
double sumBeforeModification = buffer.Sum;
int countBeforeModification = buffer.Count;
// Modify buffer after snapshot
buffer.Add(40.0);
Assert.Equal(4, buffer.Count);
// Restore to snapshot state
buffer.Restore();
Assert.Equal(countBeforeModification, buffer.Count);
Assert.Equal(sumBeforeModification, buffer.Sum);
}
[Fact]
public void Snapshot_Restore_WithWrapping()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Snapshot();
// Add value that causes wrap
buffer.Add(40.0);
Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
buffer.Restore();
Assert.Equal(60.0, buffer.Sum); // 10 + 20 + 30
Assert.Equal(30.0, buffer.Newest);
}
[Fact]
public void RecalculateSum_CorrectsDrift()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
double recalculated = buffer.RecalculateSum();
Assert.Equal(60.0, recalculated);
Assert.Equal(60.0, buffer.Sum);
}
[Fact]
public void RecalculateSum_AfterMultipleOperations()
{
var buffer = new RingBuffer(3);
// Simulate many operations that could accumulate floating-point drift
for (int i = 0; i < 100; i++)
{
buffer.Add(i * 0.1);
}
double recalculated = buffer.RecalculateSum();
// Should be equal (or very close) since we're using exact values
Assert.Equal(recalculated, buffer.Sum);
}
[Fact]
public void StartIndex_EmptyBuffer_ReturnsZero()
{
var buffer = new RingBuffer(5);
Assert.Equal(0, buffer.StartIndex);
}
[Fact]
public void StartIndex_PartiallyFilled_ReturnsZero()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
Assert.Equal(0, buffer.StartIndex);
}
[Fact]
public void StartIndex_FullBuffer_ReturnsHead()
{
var buffer = new RingBuffer(3);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
buffer.Add(40.0); // Wraps
// StartIndex should point to oldest element
Assert.True(buffer.StartIndex >= 0 && buffer.StartIndex < buffer.Capacity);
Assert.Equal(20.0, buffer.Oldest);
}
[Fact]
public void Indexer_NegativeIndexViaFromEnd_ThrowsWhenOutOfBounds()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
// ^4 when count=3 should throw
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[^4]);
}
[Fact]
public void CopyTo_InsufficientDestinationBuffer_Behavior()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
buffer.Add(30.0);
var dest = new double[2]; // Too small
// This will throw IndexOutOfRangeException since we're copying 3 elements to size-2 array
Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 0));
}
[Fact]
public void CopyTo_StartIndexOutOfRange_Behavior()
{
var buffer = new RingBuffer(5);
buffer.Add(10.0);
buffer.Add(20.0);
var dest = new double[5];
// Starting at index 4 with 2 elements should fail
Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 4));
}
}
+642
View File
@@ -0,0 +1,642 @@
using System.Collections;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// A high-performance circular buffer for double values optimized for SIMD operations.
/// Uses pinned memory and maintains running sum for O(1) average calculations.
/// </summary>
/// <remarks>
/// Key characteristics:
/// - Fixed capacity set at construction
/// - Pinned memory for SIMD compatibility
/// - O(1) Add and Sum operations via running sum
/// - SIMD-accelerated Min/Max operations
/// - Direct span access when buffer is contiguous
/// - Thread-unsafe for maximum performance
/// </remarks>
[SkipLocalsInit]
public sealed class RingBuffer : IEnumerable<double>
{
private readonly double[] _buffer;
private int _head;
private int _count;
private double _sum;
private int _savedHead;
private int _savedCount;
private double _savedSum;
private double _savedValue;
/// <summary>
/// Creates a new RingBuffer with the specified capacity.
/// Uses pinned memory for SIMD compatibility.
/// </summary>
/// <param name="capacity">Maximum number of elements (must be > 0)</param>
public RingBuffer(int capacity)
{
if (capacity <= 0)
throw new ArgumentException("Capacity must be greater than 0", nameof(capacity));
Capacity = capacity;
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
_head = 0;
_count = 0;
_sum = 0;
}
/// <summary>
/// Maximum number of elements the buffer can hold.
/// </summary>
public int Capacity { get; }
/// <summary>
/// Current number of elements in the buffer.
/// </summary>
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count;
}
/// <summary>
/// True if the buffer is full (Count == Capacity).
/// </summary>
public bool IsFull
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count == Capacity;
}
/// <summary>
/// Running sum of all elements in the buffer.
/// O(1) operation using maintained running sum.
/// </summary>
public double Sum
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _sum;
}
/// <summary>
/// Recalculates the sum by iterating over all elements.
/// Useful for correcting floating-point drift after many updates.
/// Uses GetSequencedSpans to avoid allocation when buffer wraps.
/// </summary>
public double RecalculateSum()
{
double sum = 0;
GetSequencedSpans(out var first, out var second);
for (int i = 0; i < first.Length; i++)
{
sum += first[i];
}
for (int i = 0; i < second.Length; i++)
{
sum += second[i];
}
_sum = sum;
return sum;
}
/// <summary>
/// Average of all elements in the buffer.
/// Returns 0 if buffer is empty.
/// </summary>
public double Average
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count > 0 ? _sum / _count : 0;
}
/// <summary>
/// Gets the newest (most recently added) value.
/// Returns double.NaN if buffer is empty.
/// </summary>
public double Newest
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_count == 0) return double.NaN;
int idx = (_head - 1 + Capacity) % Capacity;
return _buffer[idx];
}
}
/// <summary>
/// Gets the oldest value in the buffer.
/// Returns double.NaN if buffer is empty.
/// </summary>
public double Oldest
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_count == 0) return double.NaN;
int start = _count == Capacity ? _head : 0;
return _buffer[start];
}
}
/// <summary>
/// Gets the index in the internal buffer where the oldest element is located.
/// </summary>
public int StartIndex
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count == Capacity ? _head : 0;
}
/// <summary>
/// Gets a read-only span over the internal buffer array for direct SIMD access.
/// </summary>
public ReadOnlySpan<double> InternalBuffer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer.AsSpan();
}
/// <summary>
/// Adds a value to the buffer.
/// If full, the oldest value is overwritten and its value is subtracted from the sum.
/// Returns the value that was removed (0 if buffer was not full).
/// </summary>
/// <param name="value">Value to add</param>
/// <returns>The removed oldest value, or 0 if buffer was not full</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Add(double value)
{
double removed = 0;
if (_count == Capacity)
{
removed = _buffer[_head];
_sum = Math.FusedMultiplyAdd(-1.0, removed, _sum + value);
}
else
{
_count++;
_sum += value;
}
_buffer[_head] = value;
_head = (_head + 1) % Capacity;
return removed;
}
/// <summary>
/// Adds a value with support for bar correction semantics.
/// </summary>
/// <param name="value">Value to add</param>
/// <param name="isNew">True for new bar, false for update to current bar</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(double value, bool isNew)
{
if (isNew || _count == 0)
{
Add(value);
}
else
{
UpdateNewest(value);
}
}
/// <summary>
/// Updates the newest (most recently added) value.
/// This is used for bar correction (isNew=false semantics).
/// </summary>
/// <param name="value">New value to replace the newest</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateNewest(double value)
{
if (_count == 0) return;
int idx = (_head - 1 + Capacity) % Capacity;
double oldValue = _buffer[idx];
_sum = Math.FusedMultiplyAdd(-1.0, oldValue, _sum + value);
_buffer[idx] = value;
}
/// <summary>
/// Gets or sets element at the specified index (0 = oldest, Count-1 = newest).
/// Supports negative indexing via Index type.
/// </summary>
public double this[Index index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetAt(index);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => SetAt(index, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetAt(Index index)
{
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
#pragma warning restore S3236
int start = _count == Capacity ? _head : 0;
int bufferIdx = (start + actualIndex) % Capacity;
return _buffer[bufferIdx];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetAt(Index index, double value)
{
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
#pragma warning restore S3236
int start = _count == Capacity ? _head : 0;
int bufferIdx = (start + actualIndex) % Capacity;
double oldValue = _buffer[bufferIdx];
_sum = Math.FusedMultiplyAdd(-1.0, oldValue, _sum + value);
_buffer[bufferIdx] = value;
}
/// <summary>
/// Returns a span over the buffer contents.
/// If buffer is contiguous, returns direct span (SIMD-friendly).
/// If wrapped, returns span over a copy.
/// </summary>
/// <remarks>
/// <para><b>  Allocation Warning:</b> When the buffer wraps around (i.e., when data spans
/// from the end of the internal array back to the beginning), this method allocates a new
/// array via <see cref="ToArray"/> to return contiguous data. For allocation-free iteration
/// over wrapped buffers, use <see cref="GetSequencedSpans"/> instead.</para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<double> GetSpan()
{
if (_count == 0) return ReadOnlySpan<double>.Empty;
int start = _count == Capacity ? _head : 0;
if (start + _count <= Capacity)
{
return new ReadOnlySpan<double>(_buffer, start, _count);
}
return new ReadOnlySpan<double>(ToArray());
}
/// <summary>
/// Returns a span over the entire internal buffer (for advanced SIMD use).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<double> GetInternalSpan() => _buffer.AsSpan();
/// <summary>
/// Gets the two sequential spans that make up the buffer contents in chronological order (Oldest to Newest).
/// <param name="first">The first segment of data.</param>
/// <param name="second">The second segment of data (empty if buffer is contiguous).</param>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void GetSequencedSpans(out ReadOnlySpan<double> first, out ReadOnlySpan<double> second)
{
if (_count == 0)
{
first = default;
second = default;
return;
}
int start = _count == Capacity ? _head : 0;
int firstLen = Math.Min(_count, Capacity - start);
first = new ReadOnlySpan<double>(_buffer, start, firstLen);
second = _count > firstLen
? new ReadOnlySpan<double>(_buffer, 0, _count - firstLen)
: default;
}
/// <summary>
/// Returns the maximum value in the buffer using SIMD acceleration.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Max()
{
if (_count == 0) return double.NaN;
return MaxSimd();
}
/// <summary>
/// Returns the minimum value in the buffer using SIMD acceleration.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Min()
{
if (_count == 0) return double.NaN;
return MinSimd();
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double MaxSimd()
{
GetSequencedSpans(out var first, out var second);
var vectorSize = Vector<double>.Count;
var maxVector = new Vector<double>(double.MinValue);
double max = double.MinValue;
// Process first span with SIMD
int i = 0;
if (first.Length >= vectorSize)
{
ref double firstRef = ref MemoryMarshal.GetReference(first);
for (; i <= first.Length - vectorSize; i += vectorSize)
{
maxVector = Vector.Max(maxVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref firstRef, i)));
}
}
// Scalar remainder of first span
for (; i < first.Length; i++)
{
max = Math.Max(max, first[i]);
}
// Process second span with SIMD (if wrapped)
i = 0;
if (second.Length >= vectorSize)
{
ref double secondRef = ref MemoryMarshal.GetReference(second);
for (; i <= second.Length - vectorSize; i += vectorSize)
{
maxVector = Vector.Max(maxVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref secondRef, i)));
}
}
// Scalar remainder of second span
for (; i < second.Length; i++)
{
max = Math.Max(max, second[i]);
}
// Reduce vector to scalar
for (int j = 0; j < vectorSize; j++)
{
max = Math.Max(max, maxVector[j]);
}
return max;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double MinSimd()
{
GetSequencedSpans(out var first, out var second);
var vectorSize = Vector<double>.Count;
var minVector = new Vector<double>(double.MaxValue);
double min = double.MaxValue;
// Process first span with SIMD
int i = 0;
if (first.Length >= vectorSize)
{
ref double firstRef = ref MemoryMarshal.GetReference(first);
for (; i <= first.Length - vectorSize; i += vectorSize)
{
minVector = Vector.Min(minVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref firstRef, i)));
}
}
// Scalar remainder of first span
for (; i < first.Length; i++)
{
min = Math.Min(min, first[i]);
}
// Process second span with SIMD (if wrapped)
i = 0;
if (second.Length >= vectorSize)
{
ref double secondRef = ref MemoryMarshal.GetReference(second);
for (; i <= second.Length - vectorSize; i += vectorSize)
{
minVector = Vector.Min(minVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref secondRef, i)));
}
}
// Scalar remainder of second span
for (; i < second.Length; i++)
{
min = Math.Min(min, second[i]);
}
// Reduce vector to scalar
for (int j = 0; j < vectorSize; j++)
{
min = Math.Min(min, minVector[j]);
}
return min;
}
/// <summary>
/// Clears all elements from the buffer.
/// </summary>
public void Clear()
{
Array.Clear(_buffer, 0, _buffer.Length);
_head = 0;
_count = 0;
_sum = 0;
}
/// <summary>
/// Copies the buffer elements to a new array in chronological order.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double[] ToArray()
{
if (_count == 0) return Array.Empty<double>();
double[] array = new double[_count];
CopyTo(array, 0);
return array;
}
/// <summary>
/// Copies elements to destination array starting at destinationIndex.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(double[] destination, int destinationIndex)
{
if (_count == 0) return;
int start = _count == Capacity ? _head : 0;
if (start + _count <= Capacity)
{
Array.Copy(_buffer, start, destination, destinationIndex, _count);
}
else
{
int firstPartLength = Capacity - start;
Array.Copy(_buffer, start, destination, destinationIndex, firstPartLength);
Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _count - firstPartLength);
}
}
/// <summary>
/// Copies elements to a destination span in chronological order.
/// Destination must have at least Count elements.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(Span<double> destination)
{
if (_count == 0) return;
int start = _count == Capacity ? _head : 0;
if (start + _count <= Capacity)
{
_buffer.AsSpan(start, _count).CopyTo(destination);
}
else
{
int firstPartLength = Capacity - start;
_buffer.AsSpan(start, firstPartLength).CopyTo(destination);
_buffer.AsSpan(0, _count - firstPartLength).CopyTo(destination.Slice(firstPartLength));
}
}
/// <summary>
/// Creates a copy of the current state for bar correction support.
/// </summary>
public RingBuffer Clone()
{
var clone = new RingBuffer(Capacity);
Array.Copy(_buffer, clone._buffer, Capacity);
clone._head = _head;
clone._count = _count;
clone._sum = _sum;
return clone;
}
/// <summary>
/// Copies state from another RingBuffer.
/// Both buffers must have the same capacity.
/// </summary>
public void CopyFrom(RingBuffer source)
{
if (source.Capacity != Capacity)
throw new ArgumentException("Source buffer must have same capacity", nameof(source));
Array.Copy(source._buffer, _buffer, Capacity);
_head = source._head;
_count = source._count;
_sum = source._sum;
}
/// <summary>
/// Captures the current state of the buffer.
/// Must be called BEFORE adding a new value if you intend to Restore later.
/// Saves the value at _head position (which will be overwritten by the next Add).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Snapshot()
{
_savedHead = _head;
_savedCount = _count;
_savedSum = _sum;
// Save the value that will be overwritten by the next Add()
// When buffer is full, Add() will overwrite _buffer[_head] (the oldest value)
// When buffer is not full, _buffer[_head] is undefined but we save it anyway
_savedValue = _buffer[_head];
}
/// <summary>
/// Restores the buffer to the state captured by Snapshot.
/// This restores the buffer to its state before the last Add() operation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Restore()
{
_head = _savedHead;
_count = _savedCount;
_sum = _savedSum;
// Restore the value at _head position that was saved before the Add()
_buffer[_head] = _savedValue;
}
/// <summary>
/// Returns an enumerator that iterates through the buffer in chronological order.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new(this);
IEnumerator<double> IEnumerable<double>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// High-performance enumerator for the RingBuffer.
/// </summary>
public struct Enumerator : IEnumerator<double>, IEquatable<Enumerator>
{
private readonly RingBuffer _buffer;
private readonly int _start;
private readonly int _count;
private int _index;
private double _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(RingBuffer buffer)
{
_buffer = buffer;
_count = buffer._count;
_start = buffer._count == buffer.Capacity ? buffer._head : 0;
_index = -1;
_current = 0.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_index + 1 >= _count)
return false;
_index++;
int bufferIdx = (_start + _index) % _buffer.Capacity;
_current = _buffer._buffer[bufferIdx];
return true;
}
public readonly double Current => _current;
readonly object IEnumerator.Current => Current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_index = -1;
_current = 0.0;
}
public readonly void Dispose() { }
public readonly bool Equals(Enumerator other) =>
ReferenceEquals(_buffer, other._buffer) &&
_start == other._start &&
_count == other._count &&
_index == other._index &&
_current.Equals(other._current);
public readonly override bool Equals(object? obj) =>
obj is Enumerator other && Equals(other);
public readonly override int GetHashCode() =>
HashCode.Combine(RuntimeHelpers.GetHashCode(_buffer), _start, _count, _index, _current);
public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right);
public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right);
}
}