diff --git a/lib/core/ringbuffer/RingBuffer.cs b/lib/core/ringbuffer/RingBuffer.cs index 136a26a6..a1015935 100644 --- a/lib/core/ringbuffer/RingBuffer.cs +++ b/lib/core/ringbuffer/RingBuffer.cs @@ -136,6 +136,15 @@ public sealed class RingBuffer : IEnumerable } } + /// + /// Gets the index in the internal buffer where the oldest element is located. + /// + public int StartIndex + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _count == _capacity ? _head : 0; + } + /// /// Gets a read-only span over the internal buffer array for direct SIMD access. /// diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index efcde1ae..088a214c 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -1,5 +1,9 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; namespace QuanTAlib; @@ -376,4 +380,325 @@ public static class SimdExtensions return MinMaxScalar(span); } + + /// + /// Calculates the dot product of two spans using SIMD intrinsics. + /// Supports AVX512, AVX2, SSE2, and NEON. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double DotProduct(this ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length || a.Length == 0) return 0; + + int len = a.Length; + + // Fast path for very small kernels (avoid SIMD overhead) + if (len <= 3) + { + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + double sum = aRef * bRef; + if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); + if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); + return sum; + } + + if (Avx512F.IsSupported) + return DotProductAvx512(a, b); + + if (Avx2.IsSupported) + return DotProductAvx2(a, b); + + if (Sse2.IsSupported) + return DotProductSse2(a, b); + + if (AdvSimd.Arm64.IsSupported) + return DotProductNeon(a, b); + + double s = 0; + ref double ar = ref MemoryMarshal.GetReference(a); + ref double br = ref MemoryMarshal.GetReference(b); + + int i = 0; + // Unroll scalar loop + for (; i <= len - 4; i += 4) + { + s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + s += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1); + s += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2); + s += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3); + } + + for (; i < len; i++) + { + s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + } + return s; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductAvx512(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector512 vSum = Vector512.Zero; + Vector512 vSum2 = Vector512.Zero; + Vector512 vSum3 = Vector512.Zero; + Vector512 vSum4 = Vector512.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 32 doubles (4 vectors) at a time + if (len >= 32) + { + for (; i <= len - 32; i += 32) + { + var va1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); + var vb2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); + + var va3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 16)); + var vb3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 16)); + + var va4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 24)); + var vb4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 24)); + + vSum = Avx512F.FusedMultiplyAdd(va1, vb1, vSum); + vSum2 = Avx512F.FusedMultiplyAdd(va2, vb2, vSum2); + vSum3 = Avx512F.FusedMultiplyAdd(va3, vb3, vSum3); + vSum4 = Avx512F.FusedMultiplyAdd(va4, vb4, vSum4); + } + } + + // Process remaining vectors (8 doubles at a time) + for (; i <= len - 8; i += 8) + { + var va = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + vSum = Avx512F.FusedMultiplyAdd(va, vb, vSum); + } + + // Combine accumulators + vSum = Avx512F.Add(vSum, vSum2); + vSum3 = Avx512F.Add(vSum3, vSum4); + vSum = Avx512F.Add(vSum, vSum3); + + // Horizontal sum - reduce to Vector256, then Vector128 + Vector256 v256 = Avx512F.Add(vSum.GetLower(), vSum.GetUpper()); + Vector128 lower = v256.GetLower(); + Vector128 upper = v256.GetUpper(); + Vector128 combined = Sse2.Add(lower, upper); + double sum = combined.GetElement(0) + combined.GetElement(1); + + // Scalar remainder + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductAvx2(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector256 vSum = Vector256.Zero; + Vector256 vSum2 = Vector256.Zero; + Vector256 vSum3 = Vector256.Zero; + Vector256 vSum4 = Vector256.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 16 doubles (4 vectors) at a time + if (len >= 16) + { + for (; i <= len - 16; i += 16) + { + var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); + var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); + + var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); + var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); + + var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12)); + var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12)); + + if (Fma.IsSupported) + { + vSum = Fma.MultiplyAdd(va1, vb1, vSum); + vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); + vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3); + vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4); + } + else + { + vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1)); + vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2)); + vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3)); + vSum4 = Avx.Add(vSum4, Avx.Multiply(va4, vb4)); + } + } + } + + // Process remaining vectors (4 doubles at a time) + for (; i <= len - 4; i += 4) + { + var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = Fma.IsSupported + ? Fma.MultiplyAdd(va, vb, vSum) + : Avx.Add(vSum, Avx.Multiply(va, vb)); + } + + // Combine accumulators + vSum = Avx.Add(vSum, vSum2); + vSum3 = Avx.Add(vSum3, vSum4); + vSum = Avx.Add(vSum, vSum3); + + // Horizontal sum + Vector128 lower = vSum.GetLower(); + Vector128 upper = vSum.GetUpper(); + Vector128 combined = Sse2.Add(lower, upper); + double sum = combined.GetElement(0) + combined.GetElement(1); + + // Process remaining elements (scalar) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductNeon(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector128 vSum = Vector128.Zero; + Vector128 vSum2 = Vector128.Zero; + Vector128 vSum3 = Vector128.Zero; + Vector128 vSum4 = Vector128.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 8 doubles (4 vectors) at a time + if (len >= 8) + { + for (; i <= len - 8; i += 8) + { + var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); + var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); + + var va3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); + var vb3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); + + var va4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 6)); + var vb4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 6)); + + // NEON has FMA on ARM64 + // Since we are inside DotProductNeon which is guarded by AdvSimd.Arm64.IsSupported, + // we can assume Arm64 support. + vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va1, vb1); + vSum2 = AdvSimd.Arm64.FusedMultiplyAdd(vSum2, va2, vb2); + vSum3 = AdvSimd.Arm64.FusedMultiplyAdd(vSum3, va3, vb3); + vSum4 = AdvSimd.Arm64.FusedMultiplyAdd(vSum4, va4, vb4); + } + } + + // Process remaining vectors (2 doubles at a time) + for (; i <= len - 2; i += 2) + { + var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va, vb); + } + + // Combine accumulators + vSum = AdvSimd.Arm64.Add(vSum, vSum2); + vSum3 = AdvSimd.Arm64.Add(vSum3, vSum4); + vSum = AdvSimd.Arm64.Add(vSum, vSum3); + + // Horizontal sum (NEON has pairwise add) + double sum = AdvSimd.Arm64.AddPairwiseScalar(vSum).ToScalar(); + + // Scalar remainder (0-1 elements) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductSse2(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector128 vSum = Vector128.Zero; + Vector128 vSum2 = Vector128.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Process 4 doubles at a time using 2 accumulators + for (; i <= len - 4; i += 4) + { + var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); + var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); + + if (Fma.IsSupported) + { + vSum = Fma.MultiplyAdd(va1, vb1, vSum); + vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); + } + else + { + vSum = Sse2.Add(vSum, Sse2.Multiply(va1, vb1)); + vSum2 = Sse2.Add(vSum2, Sse2.Multiply(va2, vb2)); + } + } + + // Process remaining 2 doubles if available + if (i <= len - 2) + { + var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = Fma.IsSupported + ? Fma.MultiplyAdd(va, vb, vSum) + : Sse2.Add(vSum, Sse2.Multiply(va, vb)); + i += 2; + } + + vSum = Sse2.Add(vSum, vSum2); + double sum = vSum.GetElement(0) + vSum.GetElement(1); + + // Scalar remainder (0-1 elements) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } } diff --git a/lib/core/tbar/TBar.Notebook.dib b/lib/core/tbar/TBar.Notebook.dib deleted file mode 100644 index f0103508..00000000 --- a/lib/core/tbar/TBar.Notebook.dib +++ /dev/null @@ -1,74 +0,0 @@ -#!meta - -{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}} - -#!csharp - -// Reference the library -#r "..\..\bin\QuanTAlib.dll" - -using QuanTAlib; - -// 1. Creating a TBar -// TBar represents a single OHLCV bar (Open, High, Low, Close, Volume) -// It is an immutable struct optimized for memory and performance - -long now = DateTime.UtcNow.Ticks; -var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0); - -Console.WriteLine($"Created TBar: {bar}"); -Console.WriteLine($"Time: {bar.AsDateTime}"); -Console.WriteLine($"Open: {bar.Open}"); -Console.WriteLine($"High: {bar.High}"); -Console.WriteLine($"Low: {bar.Low}"); -Console.WriteLine($"Close: {bar.Close}"); -Console.WriteLine($"Volume: {bar.Volume}"); - -#!csharp - -// 2. Computed Properties -// TBar provides on-demand calculation of common price averages -// These are calculated when accessed, saving storage space - -Console.WriteLine($"HL2 (High+Low)/2: {bar.HL2}"); -Console.WriteLine($"OC2 (Open+Close)/2: {bar.OC2}"); -Console.WriteLine($"OHL3 (Open+High+Low)/3: {bar.OHL3}"); -Console.WriteLine($"HLC3 (High+Low+Close)/3: {bar.HLC3}"); -Console.WriteLine($"OHLC4 (Open+High+Low+Close)/4: {bar.OHLC4}"); -Console.WriteLine($"HLCC4 (High+Low+Close+Close)/4: {bar.HLCC4}"); - -#!csharp - -// 3. TValue Accessors -// You can efficiently access individual components as TValue (Time-Value pair) -// This is useful when you need to treat a specific price component as a time series point - -Console.WriteLine($"Open TValue: {bar.O}"); -Console.WriteLine($"High TValue: {bar.H}"); -Console.WriteLine($"Low TValue: {bar.L}"); -Console.WriteLine($"Close TValue: {bar.C}"); -Console.WriteLine($"Volume TValue: {bar.V}"); - -#!csharp - -// 4. Implicit Conversions -// TBar supports implicit conversions to double (Close price), TValue (Close), and DateTime - -double closePrice = bar; -TValue value = bar; -DateTime dt = bar; - -Console.WriteLine($"Implicit double (Close): {closePrice}"); -Console.WriteLine($"Implicit TValue (Close): {value}"); -Console.WriteLine($"Implicit DateTime: {dt}"); - -#!csharp - -// 5. Equality and Immutability -// Being a struct, TBar has value semantics - -var bar2 = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0); -var bar3 = new TBar(now, 101.0, 106.0, 96.0, 103.0, 1100.0); - -Console.WriteLine($"bar equals bar2? {bar == bar2}"); // True, same values -Console.WriteLine($"bar equals bar3? {bar == bar3}"); // False, different values diff --git a/lib/core/tbarseries/TBarSeries.Notebook.dib b/lib/core/tbarseries/TBarSeries.Notebook.dib deleted file mode 100644 index 9064c391..00000000 --- a/lib/core/tbarseries/TBarSeries.Notebook.dib +++ /dev/null @@ -1,82 +0,0 @@ -#!meta - -{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}} - -#!csharp - -// Reference the library -#r "..\..\bin\QuanTAlib.dll" - -using QuanTAlib; - -// 1. Creating a TBarSeries -// TBarSeries is a collection of bars stored in Structure of Arrays (SoA) format -// This layout is optimized for performance and SIMD operations - -var bars = new TBarSeries(); -long now = DateTime.UtcNow.Ticks; - -// Add a new bar -var bar1 = new TBar(now, 100, 105, 95, 102, 1000); -bars.Add(bar1, isNew: true); - -Console.WriteLine($"Added Bar 1: Count={bars.Count}"); -Console.WriteLine($"Last Close: {bars.Last.Close}"); - -#!csharp - -// 2. Streaming Updates -// TBarSeries supports updating the last bar in place -// This is crucial for real-time feeds where the current bar changes until it closes - -// Update the bar (e.g. price changed within the same minute) -var bar1Update = new TBar(now, 100, 106, 95, 104, 1500); -bars.Add(bar1Update, isNew: false); - -Console.WriteLine($"Updated Bar 1: Count={bars.Count} (Count should not increase)"); -Console.WriteLine($"Last Close: {bars.Last.Close}"); -Console.WriteLine($"Last High: {bars.Last.High}"); - -#!csharp - -// 3. Zero-Copy Views -// You can access individual components (Open, High, Low, Close, Volume) as TSeries -// These views share the underlying memory, so no copying is involved - -Console.WriteLine($"Bars Count: {bars.Count}"); -Console.WriteLine($"Close Series Count: {bars.Close.Count}"); -Console.WriteLine($"Close Series Last: {bars.Close.Last.Value}"); - -// Verify view updates automatically -Console.WriteLine("\nAdding new bar..."); -bars.Add(now + TimeSpan.TicksPerMinute, 104, 108, 103, 107, 2000, isNew: true); - -Console.WriteLine($"Bars Count: {bars.Count}"); -Console.WriteLine($"Close Series Count: {bars.Close.Count} (Should match Bars Count)"); -Console.WriteLine($"Close Series Last: {bars.Close.Last.Value} (Should be 107)"); - -#!csharp - -// 4. Aliases and Direct Access -// TBarSeries provides short aliases (O, H, L, C, V) and direct access properties - -Console.WriteLine($"Alias Access (C.Last): {bars.C.Last.Value}"); -Console.WriteLine($"Direct Last Access (LastClose): {bars.LastClose}"); -Console.WriteLine($"Direct Last Time (LastTime): {new DateTime(bars.LastTime)}"); - -#!csharp - -// 5. Iteration -// You can iterate over the bars or individual series - -Console.WriteLine("\nIterating over bars:"); -foreach (var bar in bars) -{ - Console.WriteLine($" {bar}"); -} - -Console.WriteLine("\nIterating over Close prices:"); -for (int i = 0; i < bars.Count; i++) -{ - Console.WriteLine($" Bar {i}: Close={bars.Close[i].Value}"); -} diff --git a/lib/core/tseries/TSeries.Notebook.dib b/lib/core/tseries/TSeries.Notebook.dib deleted file mode 100644 index 8d83bb75..00000000 --- a/lib/core/tseries/TSeries.Notebook.dib +++ /dev/null @@ -1,84 +0,0 @@ -#!meta - -{"kernelInfo":{"defaultKernelName":"csharp","items":[{"name":"csharp"},{"name":"fsharp","languageName":"F#","aliases":["f#","fs"]},{"name":"html","languageName":"HTML"},{"name":"http","languageName":"HTTP"},{"name":"javascript","languageName":"JavaScript","aliases":["js"]},{"name":"mermaid","languageName":"Mermaid"},{"name":"pwsh","languageName":"PowerShell","aliases":["powershell"]},{"name":"value"}]}} - -#!markdown - -# TSeries Examples - -This notebook demonstrates the usage of `TSeries`, the high-performance time series container in QuanTAlib. - -For detailed documentation, see [TSeries.md](TSeries.md). - -#!csharp - -// Reference the library -#r "..\..\bin\QuanTAlib.dll" - -using System; -using QuanTAlib; - -#!markdown - -## Creating and Adding Data - -`TSeries` supports adding data via `DateTime` or `ticks`. - -#!csharp - -var series = new TSeries(); -var now = DateTime.UtcNow; - -// Add new values -series.Add(now, 10.0); -series.Add(now.AddMinutes(1), 11.0); -series.Add(now.AddMinutes(2), 12.0); - -Console.WriteLine($"Count: {series.Count}"); -Console.WriteLine($"Last Value: {series.Last.Value}"); - -#!markdown - -## Streaming Updates (`isNew`) - -In real-time scenarios, you often receive updates for the *current* bar before it closes. `TSeries` handles this via the `isNew` parameter. - -#!csharp - -var streamSeries = new TSeries(); -long t = DateTime.UtcNow.Ticks; - -// 1. New Bar -streamSeries.Add(t, 100.0, isNew: true); -Console.WriteLine($"New Bar: Count={streamSeries.Count}, Last={streamSeries.Last.Value}"); - -// 2. Update Current Bar (Price moves to 101.0) -streamSeries.Add(t, 101.0, isNew: false); -Console.WriteLine($"Update: Count={streamSeries.Count}, Last={streamSeries.Last.Value}"); - -// 3. Update Current Bar (Price moves to 100.5) -streamSeries.Add(t, 100.5, isNew: false); -Console.WriteLine($"Update: Count={streamSeries.Count}, Last={streamSeries.Last.Value}"); - -// 4. New Bar (Next minute) -streamSeries.Add(t + TimeSpan.TicksPerMinute, 102.0, isNew: true); -Console.WriteLine($"New Bar: Count={streamSeries.Count}, Last={streamSeries.Last.Value}"); - -#!markdown - -## Zero-Copy Access (Spans) - -You can access the underlying data arrays directly as `ReadOnlySpan` for high-performance processing. - -#!csharp - -// Access Values as Span -Console.WriteLine("Values in Span:"); -foreach (var v in series.Values) -{ - Console.Write($"{v} "); -} -Console.WriteLine(); - -// Access Times as Span -Console.WriteLine($"First Time: {new DateTime(series.Times[0])}"); diff --git a/lib/trends/alma/Alma.cs b/lib/trends/alma/Alma.cs index 251d5e8b..5f78de16 100644 --- a/lib/trends/alma/Alma.cs +++ b/lib/trends/alma/Alma.cs @@ -155,90 +155,44 @@ public sealed class Alma : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateWeightedSum() { - // If buffer is not full, we only use the most recent 'count' weights? - // Standard ALMA usually waits for full period, or re-normalizes weights. - // Here we'll re-normalize based on how many items we have. - // But to match standard behavior, we usually just run on what we have. - // However, the weights are designed for a specific period. - // Using a partial window with full-period weights might be weird. - // Let's stick to the standard: use the weights corresponding to the filled positions. - // Since RingBuffer adds new items at 'head', and we want to apply weights - // such that weights[period-1] applies to the newest item, etc. - - // RingBuffer: [Oldest ... Newest] - // Weights: [0 ... period-1] - // We want: Sum(Buffer[i] * Weights[i]) / Sum(Weights) - - // BUT: If buffer is not full, say count=5, period=10. - // We have 5 items. Should we use weights[0..4] or weights[5..9]? - // Usually, moving averages grow. - // Let's assume we use the last 'count' weights, normalized. - - ReadOnlySpan bufferSpan = _buffer.GetSpan(); - int count = bufferSpan.Length; - - // If not full, we need to handle it carefully. - // For simplicity and performance, let's just iterate. - // Optimization: If full, use SIMD. - + int count = _buffer.Count; + if (count == 0) return 0; + if (count < _period) { - double sum = 0; - double wSum = 0; - // Map weights to buffer: - // Buffer[0] (oldest) -> Weights[period - count] ?? - // Actually, standard is: Weights are fixed. - // Let's align newest with newest. - // Buffer[count-1] (newest) <-> Weights[period-1] - // Buffer[0] (oldest) <-> Weights[period-count] - + // Partial buffer: align newest with newest + // Buffer[0] (oldest) -> Weights[period - count] + ReadOnlySpan bufferSpan = _buffer.GetSpan(); int weightOffset = _period - count; + + // Use DotProduct for partial sum + double sum = bufferSpan.DotProduct(_weights.AsSpan(weightOffset, count)); + + // Calculate weightSum for this subset + double wSum = 0; for (int i = 0; i < count; i++) { - double w = _weights[weightOffset + i]; - sum += bufferSpan[i] * w; - wSum += w; + wSum += _weights[weightOffset + i]; } + return wSum > 0 ? sum / wSum : 0; } - // Full buffer - return CalculateWeightedSumSimd(bufferSpan); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private double CalculateWeightedSumSimd(ReadOnlySpan buffer) - { - double sum = 0; - int i = 0; - int len = _period; - - if (Avx2.IsSupported && len >= Vector256.Count) - { - var vSum = Vector256.Zero; - ref double bufRef = ref MemoryMarshal.GetReference(buffer); - ref double wRef = ref MemoryMarshal.GetReference(_weights.AsSpan()); - - for (; i <= len - Vector256.Count; i += Vector256.Count) - { - var vBuf = Vector256.LoadUnsafe(ref Unsafe.Add(ref bufRef, i)); - var vW = Vector256.LoadUnsafe(ref Unsafe.Add(ref wRef, i)); - vSum = Avx.Add(vSum, Avx.Multiply(vBuf, vW)); - } - - // Horizontal sum - vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_01_00_11_10).AsDouble()); // skipcq: CS-R1131 - vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_00_00_00_01).AsDouble()); // skipcq: CS-R1131 - sum = vSum.GetElement(0); - } - - // Scalar fallback - for (; i < len; i++) - { - sum += buffer[i] * _weights[i]; - } - - return sum / _weightSum; + // Full buffer: use precomputed _weightSum and SIMD DotProduct + // We use InternalBuffer and StartIndex to avoid allocation and handle wrapping + ReadOnlySpan internalBuf = _buffer.InternalBuffer; + int head = _buffer.StartIndex; + + // Part 1: Oldest to End of Buffer -> InternalBuffer[Head ... Cap-1] + // Matches Weights[0 ... Cap-Head-1] + int part1Len = _period - head; + double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len)); + + // Part 2: Start of Buffer to Newest -> InternalBuffer[0 ... Head-1] + // Matches Weights[Cap-Head ... Cap-1] + double sum2 = internalBuf.Slice(0, head).DotProduct(_weights.AsSpan(part1Len)); + + return (sum1 + sum2) / _weightSum; } public static TSeries Calculate(TSeries source, int period, double offset = 0.85, double sigma = 6.0) diff --git a/lib/trends/conv/Conv.cs b/lib/trends/conv/Conv.cs index 239138b6..c72d9fe6 100644 --- a/lib/trends/conv/Conv.cs +++ b/lib/trends/conv/Conv.cs @@ -1,9 +1,6 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; namespace QuanTAlib; @@ -103,14 +100,14 @@ public sealed class Conv : ITValuePublisher if (count < _period) { - result = DotProduct(internalBuf.Slice(0, count), kernelSpan); + result = internalBuf.Slice(0, count).DotProduct(kernelSpan); } else { // Full: data is split at _head (which points to oldest) int part1Len = _period - _head; - result = DotProduct(internalBuf.Slice(_head, part1Len), kernelSpan.Slice(0, part1Len)) - + DotProduct(internalBuf.Slice(0, _head), kernelSpan.Slice(part1Len)); + result = internalBuf.Slice(_head, part1Len).DotProduct(kernelSpan.Slice(0, part1Len)) + + internalBuf.Slice(0, _head).DotProduct(kernelSpan.Slice(part1Len)); } } @@ -180,323 +177,6 @@ public sealed class Conv : ITValuePublisher return new TSeries(t, v); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProduct(ReadOnlySpan a, ReadOnlySpan b) - { - if (a.Length != b.Length || a.Length == 0) return 0; - - int len = a.Length; - - // Fast path for very small kernels (avoid SIMD overhead) - if (len <= 3) - { - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - double sum = aRef * bRef; - if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); - if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); - return sum; - } - - if (Avx512F.IsSupported) - return DotProductAvx512(a, b); - - if (Avx2.IsSupported) - return DotProductAvx2(a, b); - - if (Sse2.IsSupported) - return DotProductSse2(a, b); - - if (AdvSimd.Arm64.IsSupported) - return DotProductNeon(a, b); - - double s = 0; - ref double ar = ref MemoryMarshal.GetReference(a); - ref double br = ref MemoryMarshal.GetReference(b); - - int i = 0; - // Unroll scalar loop - for (; i <= len - 4; i += 4) - { - s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); - s += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1); - s += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2); - s += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3); - } - - for (; i < len; i++) - { - s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); - } - return s; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductAvx512(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector512 vSum = Vector512.Zero; - Vector512 vSum2 = Vector512.Zero; - Vector512 vSum3 = Vector512.Zero; - Vector512 vSum4 = Vector512.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Unroll loop: Process 32 doubles (4 vectors) at a time - if (len >= 32) - { - for (; i <= len - 32; i += 32) - { - var va1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - var va2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); - var vb2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); - - var va3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 16)); - var vb3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 16)); - - var va4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 24)); - var vb4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 24)); - - vSum = Avx512F.FusedMultiplyAdd(va1, vb1, vSum); - vSum2 = Avx512F.FusedMultiplyAdd(va2, vb2, vSum2); - vSum3 = Avx512F.FusedMultiplyAdd(va3, vb3, vSum3); - vSum4 = Avx512F.FusedMultiplyAdd(va4, vb4, vSum4); - } - } - - // Process remaining vectors (8 doubles at a time) - for (; i <= len - 8; i += 8) - { - var va = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - vSum = Avx512F.FusedMultiplyAdd(va, vb, vSum); - } - - // Combine accumulators - vSum = Avx512F.Add(vSum, vSum2); - vSum3 = Avx512F.Add(vSum3, vSum4); - vSum = Avx512F.Add(vSum, vSum3); - - // Horizontal sum - reduce to Vector256, then Vector128 - Vector256 v256 = Avx512F.Add(vSum.GetLower(), vSum.GetUpper()); - Vector128 lower = v256.GetLower(); - Vector128 upper = v256.GetUpper(); - Vector128 combined = Sse2.Add(lower, upper); - double sum = combined.GetElement(0) + combined.GetElement(1); - - // Scalar remainder - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductAvx2(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector256 vSum = Vector256.Zero; - Vector256 vSum2 = Vector256.Zero; - Vector256 vSum3 = Vector256.Zero; - Vector256 vSum4 = Vector256.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Unroll loop: Process 16 doubles (4 vectors) at a time - if (len >= 16) - { - for (; i <= len - 16; i += 16) - { - var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); - var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); - - var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); - var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); - - var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12)); - var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12)); - - if (Fma.IsSupported) - { - vSum = Fma.MultiplyAdd(va1, vb1, vSum); - vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); - vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3); - vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4); - } - else - { - vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1)); - vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2)); - vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3)); - vSum4 = Avx.Add(vSum4, Avx.Multiply(va4, vb4)); - } - } - } - - // Process remaining vectors (4 doubles at a time) - for (; i <= len - 4; i += 4) - { - var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - vSum = Fma.IsSupported - ? Fma.MultiplyAdd(va, vb, vSum) - : Avx.Add(vSum, Avx.Multiply(va, vb)); - } - - // Combine accumulators - vSum = Avx.Add(vSum, vSum2); - vSum3 = Avx.Add(vSum3, vSum4); - vSum = Avx.Add(vSum, vSum3); - - // Horizontal sum - Vector128 lower = vSum.GetLower(); - Vector128 upper = vSum.GetUpper(); - Vector128 combined = Sse2.Add(lower, upper); - double sum = combined.GetElement(0) + combined.GetElement(1); - - // Process remaining elements (scalar) - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductNeon(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector128 vSum = Vector128.Zero; - Vector128 vSum2 = Vector128.Zero; - Vector128 vSum3 = Vector128.Zero; - Vector128 vSum4 = Vector128.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Unroll loop: Process 8 doubles (4 vectors) at a time - if (len >= 8) - { - for (; i <= len - 8; i += 8) - { - var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); - var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); - - var va3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); - var vb3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); - - var va4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 6)); - var vb4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 6)); - - // NEON has FMA on ARM64 - // Since we are inside DotProductNeon which is guarded by AdvSimd.Arm64.IsSupported, - // we can assume Arm64 support. - vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va1, vb1); - vSum2 = AdvSimd.Arm64.FusedMultiplyAdd(vSum2, va2, vb2); - vSum3 = AdvSimd.Arm64.FusedMultiplyAdd(vSum3, va3, vb3); - vSum4 = AdvSimd.Arm64.FusedMultiplyAdd(vSum4, va4, vb4); - } - } - - // Process remaining vectors (2 doubles at a time) - for (; i <= len - 2; i += 2) - { - var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va, vb); - } - - // Combine accumulators - vSum = AdvSimd.Arm64.Add(vSum, vSum2); - vSum3 = AdvSimd.Arm64.Add(vSum3, vSum4); - vSum = AdvSimd.Arm64.Add(vSum, vSum3); - - // Horizontal sum (NEON has pairwise add) - double sum = AdvSimd.Arm64.AddPairwiseScalar(vSum).ToScalar(); - - // Scalar remainder (0-1 elements) - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductSse2(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector128 vSum = Vector128.Zero; - Vector128 vSum2 = Vector128.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Process 4 doubles at a time using 2 accumulators - for (; i <= len - 4; i += 4) - { - var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); - var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); - - if (Fma.IsSupported) - { - vSum = Fma.MultiplyAdd(va1, vb1, vSum); - vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); - } - else - { - vSum = Sse2.Add(vSum, Sse2.Multiply(va1, vb1)); - vSum2 = Sse2.Add(vSum2, Sse2.Multiply(va2, vb2)); - } - } - - // Process remaining 2 doubles if available - if (i <= len - 2) - { - var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - vSum = Fma.IsSupported - ? Fma.MultiplyAdd(va, vb, vSum) - : Sse2.Add(vSum, Sse2.Multiply(va, vb)); - i += 2; - } - - vSum = Sse2.Add(vSum, vSum2); - double sum = vSum.GetElement(0) + vSum.GetElement(1); - - // Scalar remainder (0-1 elements) - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - public static TSeries Calculate(TSeries source, double[] kernel) { var conv = new Conv(kernel); @@ -548,14 +228,14 @@ public sealed class Conv : ITValuePublisher { int kernelOffset = period - count; // Window is [0..count-1] - sum = DotProduct(window.Slice(0, count), kernelSpan.Slice(kernelOffset)); + sum = window.Slice(0, count).DotProduct(kernelSpan.Slice(kernelOffset)); } else { // Full buffer - branchless version int part1Len = period - windowIdx; - sum = DotProduct(window.Slice(windowIdx, part1Len), kernelSpan.Slice(0, part1Len)) - + DotProduct(window.Slice(0, windowIdx), kernelSpan.Slice(part1Len)); + sum = window.Slice(windowIdx, part1Len).DotProduct(kernelSpan.Slice(0, part1Len)) + + window.Slice(0, windowIdx).DotProduct(kernelSpan.Slice(part1Len)); } output[i] = sum; diff --git a/lib/trends/sma/Sma.cs b/lib/trends/sma/Sma.cs index e6b7fa1c..e442de07 100644 --- a/lib/trends/sma/Sma.cs +++ b/lib/trends/sma/Sma.cs @@ -225,7 +225,7 @@ public sealed class Sma : ITValuePublisher // Try SIMD path for large, clean datasets // Requirements: AVX2 support, large enough dataset, no NaN values const int SimdThreshold = 256; - if (Avx2.IsSupported && len >= SimdThreshold && !HasNonFiniteValues(source)) + if (Avx2.IsSupported && len >= SimdThreshold && !source.ContainsNonFinite()) { CalculateSimdCore(source, output, period); return; @@ -367,20 +367,6 @@ public sealed class Sma : ITValuePublisher } } - /// - /// Checks if span contains any non-finite values (NaN or Infinity). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasNonFiniteValues(ReadOnlySpan span) - { - for (int idx = 0; idx < span.Length; idx++) - { - if (!double.IsFinite(span[idx])) - return true; - } - return false; - } - /// /// Resets the SMA state. /// diff --git a/lib/trends/wma/Wma.cs b/lib/trends/wma/Wma.cs index 028edb03..5e8b6bd9 100644 --- a/lib/trends/wma/Wma.cs +++ b/lib/trends/wma/Wma.cs @@ -213,7 +213,7 @@ public sealed class Wma : ITValuePublisher if (len == 0) return; const int SimdThreshold = 256; - if (Avx2.IsSupported && len >= SimdThreshold && !HasNonFiniteValues(source)) + if (Avx2.IsSupported && len >= SimdThreshold && !source.ContainsNonFinite()) { CalculateSimdCore(source, output, period); return; @@ -477,17 +477,6 @@ public sealed class Wma : ITValuePublisher } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasNonFiniteValues(ReadOnlySpan span) - { - for (int idx = 0; idx < span.Length; idx++) - { - if (!double.IsFinite(span[idx])) - return true; - } - return false; - } - public void Reset() { _buffer.Clear();