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
File diff suppressed because it is too large Load Diff
+995
View File
@@ -0,0 +1,995 @@
namespace QuanTAlib.Tests;
public class SimdExtensionsTests
{
// ContainsNonFinite tests
[Fact]
public void ContainsNonFinite_EmptySpan_ReturnsFalse()
{
var span = ReadOnlySpan<double>.Empty;
Assert.False(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_AllFinite_ReturnsFalse()
{
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
var span = new ReadOnlySpan<double>(data);
Assert.False(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_ContainsNaN_ReturnsTrue()
{
double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0, 6.0, 7.0, 8.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_ContainsPositiveInfinity_ReturnsTrue()
{
double[] data = [1.0, 2.0, 3.0, double.PositiveInfinity, 5.0, 6.0, 7.0, 8.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_ContainsNegativeInfinity_ReturnsTrue()
{
double[] data = [1.0, 2.0, 3.0, 4.0, double.NegativeInfinity, 6.0, 7.0, 8.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_NonFiniteInRemainder_ReturnsTrue()
{
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, double.NaN];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_SingleNaN_ReturnsTrue()
{
double[] data = [double.NaN];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_TwoElements_AllFinite_ReturnsFalse()
{
double[] data = [1.0, 2.0];
var span = new ReadOnlySpan<double>(data);
Assert.False(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_TwoElements_OneNaN_ReturnsTrue()
{
double[] data = [1.0, double.NaN];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
// SumSIMD tests
[Fact]
public void SumSIMD_EmptySpan_ReturnsZero()
{
var span = ReadOnlySpan<double>.Empty;
Assert.Equal(0.0, span.SumSIMD());
}
[Fact]
public void SumSIMD_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, span.SumSIMD());
}
[Fact]
public void SumSIMD_TwoElements_ReturnsSum()
{
double[] data = [1.5, 2.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(4.0, span.SumSIMD());
}
[Fact]
public void SumSIMD_MultipleElements_ReturnsCorrectSum()
{
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(55.0, span.SumSIMD(), precision: 10);
}
[Fact]
public void SumSIMD_LargeArray_ReturnsCorrectSum()
{
double[] data = new double[1000];
for (int i = 0; i < data.Length; i++)
data[i] = i + 1.0;
var span = new ReadOnlySpan<double>(data);
const double expected = 1000.0 * 1001.0 / 2.0;
Assert.Equal(expected, span.SumSIMD(), precision: 8);
}
[Fact]
public void SumSIMD_ContainsNaN_ReturnsNaN()
{
double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.SumSIMD()));
}
[Fact]
public void SumSIMD_ContainsInfinity_ReturnsNaN()
{
double[] data = [1.0, 2.0, double.PositiveInfinity, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.SumSIMD()));
}
[Fact]
public void SumSIMD_NegativeValues_ReturnsCorrectSum()
{
double[] data = [-1.0, -2.0, -3.0, -4.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(-10.0, span.SumSIMD());
}
// MinSIMD tests
[Fact]
public void MinSIMD_EmptySpan_ReturnsNaN()
{
var span = ReadOnlySpan<double>.Empty;
Assert.True(double.IsNaN(span.MinSIMD()));
}
[Fact]
public void MinSIMD_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, span.MinSIMD());
}
[Fact]
public void MinSIMD_TwoElements_ReturnsMinimum()
{
double[] data = [5.0, 2.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(2.0, span.MinSIMD());
}
[Fact]
public void MinSIMD_MultipleElements_ReturnsMinimum()
{
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(1.0, span.MinSIMD());
}
[Fact]
public void MinSIMD_ContainsNaN_ReturnsNaN()
{
double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.MinSIMD()));
}
[Fact]
public void MinSIMD_MinInRemainder_ReturnsCorrectMin()
{
double[] data = [5.0, 2.0, 8.0, 6.0, 9.0, 3.0, 7.0, 4.0, 0.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(0.5, span.MinSIMD());
}
[Fact]
public void MinSIMD_NegativeValues_ReturnsMinimum()
{
double[] data = [-5.0, -2.0, -8.0, -1.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(-8.0, span.MinSIMD());
}
// MaxSIMD tests
[Fact]
public void MaxSIMD_EmptySpan_ReturnsNaN()
{
var span = ReadOnlySpan<double>.Empty;
Assert.True(double.IsNaN(span.MaxSIMD()));
}
[Fact]
public void MaxSIMD_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, span.MaxSIMD());
}
[Fact]
public void MaxSIMD_TwoElements_ReturnsMaximum()
{
double[] data = [5.0, 9.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(9.0, span.MaxSIMD());
}
[Fact]
public void MaxSIMD_MultipleElements_ReturnsMaximum()
{
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(9.0, span.MaxSIMD());
}
[Fact]
public void MaxSIMD_ContainsNaN_ReturnsNaN()
{
double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.MaxSIMD()));
}
[Fact]
public void MaxSIMD_MaxInRemainder_ReturnsCorrectMax()
{
double[] data = [5.0, 2.0, 8.0, 6.0, 4.0, 3.0, 7.0, 1.0, 99.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(99.0, span.MaxSIMD());
}
[Fact]
public void MaxSIMD_NegativeValues_ReturnsMaximum()
{
double[] data = [-5.0, -2.0, -8.0, -1.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(-1.0, span.MaxSIMD());
}
// AverageSIMD tests
[Fact]
public void AverageSIMD_EmptySpan_ReturnsNaN()
{
var span = ReadOnlySpan<double>.Empty;
Assert.True(double.IsNaN(span.AverageSIMD()));
}
[Fact]
public void AverageSIMD_TwoElements_ReturnsAverage()
{
double[] data = [2.0, 4.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(3.0, span.AverageSIMD());
}
[Fact]
public void AverageSIMD_MultipleElements_ReturnsCorrectAverage()
{
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(3.0, span.AverageSIMD(), precision: 10);
}
[Fact]
public void AverageSIMD_ContainsNaN_ReturnsNaN()
{
double[] data = [1.0, double.NaN, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.AverageSIMD()));
}
// VarianceSIMD tests
[Fact]
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.VarianceSIMD()));
}
[Fact]
public void VarianceSIMD_EmptySpan_ReturnsNaN()
{
var span = ReadOnlySpan<double>.Empty;
Assert.True(double.IsNaN(span.VarianceSIMD()));
}
[Fact]
public void VarianceSIMD_TwoElements_ReturnsCorrect()
{
double[] data = [1.0, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(2.0, span.VarianceSIMD(), precision: 10);
}
[Fact]
public void VarianceSIMD_ThreeElements_ReturnsCorrect()
{
double[] data = [1.0, 2.0, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(1.0, span.VarianceSIMD(), precision: 10);
}
[Fact]
public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance()
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double variance = span.VarianceSIMD();
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
}
[Fact]
public void VarianceSIMD_WithProvidedMean_UsesProvidedMean()
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double mean = 5.0;
double variance = span.VarianceSIMD(mean);
Assert.True(variance > 0);
}
[Fact]
public void VarianceSIMD_ContainsNaN_ReturnsNaN()
{
double[] data = [2.0, double.NaN, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.VarianceSIMD()));
}
[Fact]
public void VarianceSIMD_WithNaNMean_ReturnsNaN()
{
double[] data = [2.0, 4.0, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.VarianceSIMD(double.NaN)));
}
[Fact]
public void VarianceSIMD_WithInfinityMean_ReturnsNaN()
{
double[] data = [2.0, 4.0, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.VarianceSIMD(double.PositiveInfinity)));
}
// StdDevSIMD tests
[Fact]
public void StdDevSIMD_TwoElements_ReturnsCorrect()
{
double[] data = [1.0, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(Math.Abs(span.StdDevSIMD() - 1.414) < 0.01);
}
[Fact]
public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev()
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double stdDev = span.StdDevSIMD();
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
}
[Fact]
public void StdDevSIMD_WithProvidedMean_Works()
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double stdDev = span.StdDevSIMD(5.0);
Assert.True(stdDev > 0);
}
[Fact]
public void StdDevSIMD_ContainsNaN_ReturnsNaN()
{
double[] data = [2.0, double.NaN, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.StdDevSIMD()));
}
// MinMaxSIMD tests
[Fact]
public void MinMaxSIMD_EmptySpan_ReturnsBothNaN()
{
var span = ReadOnlySpan<double>.Empty;
var (min, max) = span.MinMaxSIMD();
Assert.True(double.IsNaN(min));
Assert.True(double.IsNaN(max));
}
[Fact]
public void MinMaxSIMD_SingleElement_ReturnsSameValue()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
var (min, max) = span.MinMaxSIMD();
Assert.Equal(42.5, min);
Assert.Equal(42.5, max);
}
[Fact]
public void MinMaxSIMD_TwoElements_ReturnsCorrect()
{
double[] data = [5.0, 2.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = span.MinMaxSIMD();
Assert.Equal(2.0, min);
Assert.Equal(5.0, max);
}
[Fact]
public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax()
{
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = span.MinMaxSIMD();
Assert.Equal(1.0, min);
Assert.Equal(9.0, max);
}
[Fact]
public void MinMaxSIMD_ContainsNaN_ReturnsBothNaN()
{
double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = span.MinMaxSIMD();
Assert.True(double.IsNaN(min));
Assert.True(double.IsNaN(max));
}
[Fact]
public void MinMaxSIMD_MinMaxInRemainder_ReturnsCorrect()
{
double[] data = [5.0, 2.0, 8.0, 6.0, 4.0, 3.0, 7.0, 5.0, 0.1, 99.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = span.MinMaxSIMD();
Assert.Equal(0.1, min);
Assert.Equal(99.0, max);
}
[Fact]
public void MinMaxSIMD_NegativeValues_ReturnsCorrect()
{
double[] data = [-5.0, -2.0, -8.0, -1.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = span.MinMaxSIMD();
Assert.Equal(-8.0, min);
Assert.Equal(-1.0, max);
}
// Add/Subtract tests
[Fact]
public void Add_SameLength_CorrectResult()
{
double[] left = [1.0, 2.0, 3.0, 4.0, 5.0];
double[] right = [10.0, 20.0, 30.0, 40.0, 50.0];
double[] result = new double[5];
SimdExtensions.Add(left, right, result);
Assert.Equal(11.0, result[0]);
Assert.Equal(22.0, result[1]);
Assert.Equal(33.0, result[2]);
Assert.Equal(44.0, result[3]);
Assert.Equal(55.0, result[4]);
}
[Fact]
public void Add_DifferentLengths_ThrowsArgumentException()
{
double[] left = [1.0, 2.0];
double[] right = [1.0];
double[] result = new double[2];
Assert.Throws<ArgumentException>(() => SimdExtensions.Add(left, right, result));
}
[Fact]
public void Subtract_SameLength_CorrectResult()
{
double[] left = [10.0, 20.0, 30.0, 40.0, 50.0];
double[] right = [1.0, 2.0, 3.0, 4.0, 5.0];
double[] result = new double[5];
SimdExtensions.Subtract(left, right, result);
Assert.Equal(9.0, result[0]);
Assert.Equal(18.0, result[1]);
Assert.Equal(27.0, result[2]);
Assert.Equal(36.0, result[3]);
Assert.Equal(45.0, result[4]);
}
[Fact]
public void Subtract_DifferentLengths_ThrowsArgumentException()
{
double[] left = [1.0, 2.0];
double[] right = [1.0];
double[] result = new double[2];
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
}
// DotProduct tests
[Fact]
public void DotProduct_SameLength_CorrectResult()
{
double[] a = [1.0, 2.0, 3.0];
double[] b = [4.0, 5.0, 6.0];
// 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32
Assert.Equal(32.0, a.DotProduct(b));
}
[Fact]
public void DotProduct_DifferentLengths_ThrowsArgumentException()
{
double[] a = [1.0, 2.0];
double[] b = [1.0];
Assert.Throws<ArgumentException>(() => a.DotProduct(b));
}
[Fact]
public void DotProduct_EmptySpans_ReturnsZero()
{
double[] a = [];
double[] b = [];
Assert.Equal(0.0, a.DotProduct(b));
}
// Integration tests
[Fact]
public void SIMD_WorksWithTSeriesValues()
{
var series = new TSeries(100);
for (int i = 0; i < 100; i++)
{
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
}
var values = series.Values;
double sum = values.SumSIMD();
double avg = values.AverageSIMD();
double min = values.MinSIMD();
double max = values.MaxSIMD();
var (minAlt, maxAlt) = values.MinMaxSIMD();
Assert.Equal(5050.0, sum, precision: 8);
Assert.Equal(50.5, avg, precision: 8);
Assert.Equal(1.0, min);
Assert.Equal(100.0, max);
Assert.Equal(min, minAlt);
Assert.Equal(max, maxAlt);
}
[Fact]
public void SIMD_WorksWithTBarSeriesClose()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var bars = gbm.Fetch(1000, startTime, interval);
var closeValues = bars.Close.Values;
double sum = closeValues.SumSIMD();
double avg = closeValues.AverageSIMD();
double min = closeValues.MinSIMD();
double max = closeValues.MaxSIMD();
Assert.True(sum > 0);
Assert.True(avg > 0);
Assert.True(min > 0);
Assert.True(max > min);
}
[Fact]
public void SIMD_PerformanceTest_LargeDataset()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var bars = gbm.Fetch(10000, startTime, interval);
var closeValues = bars.Close.Values;
_ = closeValues.SumSIMD();
var sw = System.Diagnostics.Stopwatch.StartNew();
double sum = closeValues.SumSIMD();
double avg = closeValues.AverageSIMD();
double min = closeValues.MinSIMD();
double max = closeValues.MaxSIMD();
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
double variance = closeValues.VarianceSIMD();
double stdDev = closeValues.StdDevSIMD();
sw.Stop();
Assert.True(sum > 0);
Assert.True(avg > 0);
Assert.True(min > 0);
Assert.True(max > min);
Assert.Equal(min, minAlt);
Assert.Equal(max, maxAlt);
Assert.True(variance > 0);
Assert.True(stdDev > 0);
Assert.True(sw.ElapsedMilliseconds < 50,
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 50ms");
}
[Fact]
public void SIMD_ScalarFallback_SmallArray()
{
double[] data = [1.0, 2.0, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(6.0, span.SumSIMD());
Assert.Equal(1.0, span.MinSIMD());
Assert.Equal(3.0, span.MaxSIMD());
Assert.Equal(2.0, span.AverageSIMD());
var (min, max) = span.MinMaxSIMD();
Assert.Equal(1.0, min);
Assert.Equal(3.0, max);
}
}
// Tests for internal scalar implementations
public class SimdScalarFallbackTests
{
[Fact]
public void ContainsNonFiniteScalar_AllFinite_ReturnsFalse()
{
double[] data = [1.0, 2.0, 3.0, 4.0];
var span = new ReadOnlySpan<double>(data);
Assert.False(SimdExtensions.ContainsNonFiniteScalar(span));
}
[Fact]
public void ContainsNonFiniteScalar_ContainsNaN_ReturnsTrue()
{
double[] data = [1.0, double.NaN, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(SimdExtensions.ContainsNonFiniteScalar(span));
}
[Fact]
public void ContainsNonFiniteScalar_ContainsInfinity_ReturnsTrue()
{
double[] data = [1.0, double.PositiveInfinity, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(SimdExtensions.ContainsNonFiniteScalar(span));
}
[Fact]
public void ContainsNonFiniteScalar_Empty_ReturnsFalse()
{
var span = ReadOnlySpan<double>.Empty;
Assert.False(SimdExtensions.ContainsNonFiniteScalar(span));
}
[Fact]
public void SumScalar_MultipleElements_ReturnsCorrectSum()
{
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(15.0, SimdExtensions.SumScalar(span));
}
[Fact]
public void SumScalar_NegativeValues_ReturnsCorrectSum()
{
double[] data = [-1.0, -2.0, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(0.0, SimdExtensions.SumScalar(span));
}
[Fact]
public void SumScalar_Empty_ReturnsZero()
{
var span = ReadOnlySpan<double>.Empty;
Assert.Equal(0.0, SimdExtensions.SumScalar(span));
}
[Fact]
public void MinScalar_MultipleElements_ReturnsMinimum()
{
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(1.0, SimdExtensions.MinScalar(span));
}
[Fact]
public void MinScalar_NegativeValues_ReturnsMinimum()
{
double[] data = [-5.0, -2.0, -8.0, -1.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(-8.0, SimdExtensions.MinScalar(span));
}
[Fact]
public void MinScalar_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, SimdExtensions.MinScalar(span));
}
[Fact]
public void MaxScalar_MultipleElements_ReturnsMaximum()
{
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(9.0, SimdExtensions.MaxScalar(span));
}
[Fact]
public void MaxScalar_NegativeValues_ReturnsMaximum()
{
double[] data = [-5.0, -2.0, -8.0, -1.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(-1.0, SimdExtensions.MaxScalar(span));
}
[Fact]
public void MaxScalar_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, SimdExtensions.MaxScalar(span));
}
[Fact]
public void VarianceScalar_MultipleElements_ReturnsCorrectVariance()
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double mean = 5.0;
double variance = SimdExtensions.VarianceScalar(span, mean);
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
}
[Fact]
public void VarianceScalar_TwoElements_ReturnsCorrectVariance()
{
double[] data = [1.0, 3.0];
var span = new ReadOnlySpan<double>(data);
double mean = 2.0;
Assert.Equal(2.0, SimdExtensions.VarianceScalar(span, mean), precision: 10);
}
[Fact]
public void MinMaxScalar_MultipleElements_ReturnsCorrectMinMax()
{
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = SimdExtensions.MinMaxScalar(span);
Assert.Equal(1.0, min);
Assert.Equal(9.0, max);
}
[Fact]
public void MinMaxScalar_NegativeValues_ReturnsCorrectMinMax()
{
double[] data = [-5.0, -2.0, -8.0, -1.0];
var span = new ReadOnlySpan<double>(data);
var (min, max) = SimdExtensions.MinMaxScalar(span);
Assert.Equal(-8.0, min);
Assert.Equal(-1.0, max);
}
[Fact]
public void MinMaxScalar_SingleElement_ReturnsSameValue()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
var (min, max) = SimdExtensions.MinMaxScalar(span);
Assert.Equal(42.5, min);
Assert.Equal(42.5, max);
}
// Additional edge case tests
[Fact]
public void DotProduct_ContainsNaN_PropagatesNaN()
{
double[] a = [1.0, double.NaN, 3.0];
double[] b = [4.0, 5.0, 6.0];
double result = a.DotProduct(b);
Assert.True(double.IsNaN(result));
}
[Fact]
public void DotProduct_ContainsInfinity_PropagatesCorrectly()
{
double[] a = [1.0, double.PositiveInfinity, 3.0];
double[] b = [4.0, 5.0, 6.0];
double result = a.DotProduct(b);
Assert.True(double.IsPositiveInfinity(result));
}
[Fact]
public void Add_ContainsNaN_PropagatesNaN()
{
double[] left = [1.0, double.NaN, 3.0];
double[] right = [4.0, 5.0, 6.0];
double[] result = new double[3];
SimdExtensions.Add(left, right, result);
Assert.Equal(5.0, result[0]);
Assert.True(double.IsNaN(result[1]));
Assert.Equal(9.0, result[2]);
}
[Fact]
public void Subtract_ContainsNaN_PropagatesNaN()
{
double[] left = [10.0, double.NaN, 30.0];
double[] right = [1.0, 2.0, 3.0];
double[] result = new double[3];
SimdExtensions.Subtract(left, right, result);
Assert.Equal(9.0, result[0]);
Assert.True(double.IsNaN(result[1]));
Assert.Equal(27.0, result[2]);
}
[Fact]
public void ContainsNonFinite_NegativeInfinityAtStart_ReturnsTrue()
{
double[] data = [double.NegativeInfinity, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void ContainsNonFinite_NegativeInfinityAtEnd_ReturnsTrue()
{
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, double.NegativeInfinity];
var span = new ReadOnlySpan<double>(data);
Assert.True(span.ContainsNonFinite());
}
[Fact]
public void VarianceSIMD_SingleElement_ReturnsNaN()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.VarianceSIMD()));
}
[Fact]
public void StdDevSIMD_SingleElement_ReturnsNaN()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.True(double.IsNaN(span.StdDevSIMD()));
}
[Fact]
public void StdDevSIMD_EmptySpan_ReturnsNaN()
{
var span = ReadOnlySpan<double>.Empty;
Assert.True(double.IsNaN(span.StdDevSIMD()));
}
[Fact]
public void SumSIMD_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, span.SumSIMD());
}
[Fact]
public void AverageSIMD_SingleElement_ReturnsElement()
{
double[] data = [42.5];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(42.5, span.AverageSIMD());
}
[Fact]
public void DotProduct_SingleElement_ReturnsProduct()
{
double[] a = [3.0];
double[] b = [4.0];
Assert.Equal(12.0, a.DotProduct(b));
}
[Fact]
public void DotProduct_TwoElements_ReturnsCorrect()
{
double[] a = [2.0, 3.0];
double[] b = [4.0, 5.0];
// 2*4 + 3*5 = 8 + 15 = 23
Assert.Equal(23.0, a.DotProduct(b));
}
[Fact]
public void Add_SingleElement_Works()
{
double[] left = [5.0];
double[] right = [3.0];
double[] result = new double[1];
SimdExtensions.Add(left, right, result);
Assert.Equal(8.0, result[0]);
}
[Fact]
public void Subtract_SingleElement_Works()
{
double[] left = [5.0];
double[] right = [3.0];
double[] result = new double[1];
SimdExtensions.Subtract(left, right, result);
Assert.Equal(2.0, result[0]);
}
[Fact]
public void Add_EmptyArrays_Works()
{
double[] left = [];
double[] right = [];
double[] result = [];
SimdExtensions.Add(left, right, result); // Should not throw
Assert.Empty(result);
}
[Fact]
public void Subtract_EmptyArrays_Works()
{
double[] left = [];
double[] right = [];
double[] result = [];
SimdExtensions.Subtract(left, right, result); // Should not throw
Assert.Empty(result);
}
[Fact]
public void Add_ResultTooSmall_ThrowsArgumentException()
{
double[] left = [1.0, 2.0, 3.0];
double[] right = [4.0, 5.0, 6.0];
double[] result = new double[2]; // Too small
Assert.Throws<ArgumentException>(() => SimdExtensions.Add(left, right, result));
}
[Fact]
public void Subtract_ResultTooSmall_ThrowsArgumentException()
{
double[] left = [1.0, 2.0, 3.0];
double[] right = [4.0, 5.0, 6.0];
double[] result = new double[2]; // Too small
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
}
}
+760
View File
@@ -0,0 +1,760 @@
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;
/// <summary>
/// SIMD-accelerated extension methods for high-performance array operations.
/// Uses Vector<T> for 4-8x speedup on supported hardware with automatic scalar fallback.
/// </summary>
public static class SimdExtensions
{
// Internal scalar implementations for testability
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool ContainsNonFiniteScalar(ReadOnlySpan<double> span)
{
for (int i = 0; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static double SumScalar(ReadOnlySpan<double> span)
{
double scalar = 0.0;
for (int i = 0; i < span.Length; i++)
scalar += span[i];
return scalar;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static double MinScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
throw new ArgumentException("Span must not be empty", nameof(span));
double min = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] < min)
min = span[i];
}
return min;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static double MaxScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
throw new ArgumentException("Span must not be empty", nameof(span));
double max = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] > max)
max = span[i];
}
return max;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static double VarianceScalar(ReadOnlySpan<double> span, double mean)
{
// Match VarianceSIMD behavior: return 0.0 for length <= 1 to avoid divide-by-zero
if (span.Length <= 1)
return 0.0;
double sumSquares = 0.0;
for (int i = 0; i < span.Length; i++)
{
double diff = span[i] - mean;
sumSquares += diff * diff;
}
return sumSquares / (span.Length - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan<double> span)
{
if (span.Length == 0)
throw new ArgumentException("Span must not be empty", nameof(span));
double scalarMin = span[0];
double scalarMax = span[0];
for (int i = 1; i < span.Length; i++)
{
if (span[i] < scalarMin) scalarMin = span[i];
if (span[i] > scalarMax) scalarMax = span[i];
}
return (scalarMin, scalarMax);
}
/// <summary>
/// Checks if span contains any non-finite values (NaN or Infinity).
/// Returns true if any non-finite value is found.
/// Uses SIMD: NaN detected via v != v (NaN is the only value where this is true),
/// Infinity detected via |v| > MaxValue comparison.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsNonFinite(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return false;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
int i = 0;
var maxValue = new Vector<double>(double.MaxValue);
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
// NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes
var nanCheck = Vector.Equals(vector, vector);
if (!nanCheck.Equals(Vector<long>.AllBitsSet))
return true;
// Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue)
var absVec = Vector.Abs(vector);
var infCheck = Vector.GreaterThan(absVec, maxValue);
if (!infCheck.Equals(Vector<long>.Zero))
return true;
}
for (; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
return true;
}
return false;
}
return ContainsNonFiniteScalar(span);
}
/// <summary>
/// Calculates sum using SIMD vectorization when available.
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// Uses lazy non-finite check: computes sum first, then validates result.
/// If result is non-finite, falls back to explicit check.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SumSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return 0.0;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
Vector<double> sum = Vector<double>.Zero;
int vectorSize = Vector<double>.Count;
int i = 0;
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
sum += vector;
}
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
result += sum[j];
for (; i < span.Length; i++)
result += span[i];
// Lazy check: if result is non-finite AND input contained non-finite values, return NaN
// NaN + anything = NaN, Inf + anything finite = Inf
// If result is infinite from overflow (no input NaN/Inf), return as-is
if (!double.IsFinite(result) && span.ContainsNonFinite())
return double.NaN;
return result;
}
// Scalar path with lazy check
double scalarSum = SumScalar(span);
return !double.IsFinite(scalarSum) && span.ContainsNonFinite() ? double.NaN : scalarSum;
}
/// <summary>
/// Calculates minimum value using SIMD vectorization when available.
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MinSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
var minVec = new Vector<double>(span[..vectorSize]);
int i = vectorSize;
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
minVec = Vector.Min(minVec, vector);
}
double result = minVec[0];
for (int j = 1; j < vectorSize; j++)
{
if (minVec[j] < result)
result = minVec[j];
}
for (; i < span.Length; i++)
{
if (span[i] < result)
result = span[i];
}
return result;
}
return MinScalar(span);
}
/// <summary>
/// Calculates maximum value using SIMD vectorization when available.
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MaxSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
var maxVec = new Vector<double>(span[..vectorSize]);
int i = vectorSize;
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
maxVec = Vector.Max(maxVec, vector);
}
double result = maxVec[0];
for (int j = 1; j < vectorSize; j++)
{
if (maxVec[j] > result)
result = maxVec[j];
}
for (; i < span.Length; i++)
{
if (span[i] > result)
result = span[i];
}
return result;
}
return MaxScalar(span);
}
/// <summary>
/// Calculates average using SIMD vectorization when available.
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double AverageSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
// SumSIMD already guards against non-finite, which will propagate NaN
return span.SumSIMD() / span.Length;
}
/// <summary>
/// Calculates variance using a two-pass SIMD variant that computes the mean first (via AverageSIMD) and then sums squared differences to produce variance.
/// Note that this is not the single-pass Welford algorithm.
/// Returns NaN if any input value is non-finite or if mean is non-finite.
/// Caches non-finite check result: when mean is not provided, AverageSIMD -> SumSIMD already validates;
/// when mean IS provided, we need explicit check only once.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
{
if (span.Length < 2) return double.NaN;
double m;
if (mean.HasValue)
{
// Mean provided externally - need explicit non-finite check
if (span.ContainsNonFinite()) return double.NaN;
m = mean.Value;
}
else
{
// AverageSIMD -> SumSIMD already performs lazy non-finite check
// If input has NaN, SumSIMD returns NaN, which propagates here
m = span.AverageSIMD();
}
// If mean is NaN (from input NaN or explicit NaN mean), return NaN
if (!double.IsFinite(m)) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
var meanVec = new Vector<double>(m);
Vector<double> sumSq = Vector<double>.Zero;
int vectorSize = Vector<double>.Count;
int i = 0;
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
var diff = vector - meanVec;
sumSq += diff * diff;
}
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
result += sumSq[j];
for (; i < span.Length; i++)
{
double diff = span[i] - m;
result += diff * diff;
}
return result / (span.Length - 1);
}
return VarianceScalar(span, m);
}
/// <summary>
/// Calculates standard deviation using SIMD vectorization.
/// Returns NaN if any input value is non-finite or if mean is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
{
// VarianceSIMD already guards against non-finite, which will propagate NaN through Sqrt
return Math.Sqrt(span.VarianceSIMD(mean));
}
/// <summary>
/// Finds both min and max in a single pass using SIMD vectorization.
/// More efficient than calling MinSIMD and MaxSIMD separately.
/// Returns (NaN, NaN) if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return (double.NaN, double.NaN);
if (span.Length == 1) return (span[0], span[0]);
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return (double.NaN, double.NaN);
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
var minVec = new Vector<double>(span[..vectorSize]);
var maxVec = minVec;
int i = vectorSize;
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
minVec = Vector.Min(minVec, vector);
maxVec = Vector.Max(maxVec, vector);
}
double min = minVec[0];
double max = maxVec[0];
for (int j = 1; j < vectorSize; j++)
{
if (minVec[j] < min) min = minVec[j];
if (maxVec[j] > max) max = maxVec[j];
}
for (; i < span.Length; i++)
{
if (span[i] < min) min = span[i];
if (span[i] > max) max = span[i];
}
return (min, max);
}
return MinMaxScalar(span);
}
/// <summary>
/// Element-wise addition of two spans using SIMD.
/// result[i] = left[i] + right[i]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
throw new ArgumentException("All spans must have the same length", nameof(result));
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
for (; i <= left.Length - vectorSize; i += vectorSize)
{
var vLeft = new Vector<double>(left.Slice(i, vectorSize));
var vRight = new Vector<double>(right.Slice(i, vectorSize));
(vLeft + vRight).CopyTo(result.Slice(i, vectorSize));
}
}
for (; i < left.Length; i++)
{
result[i] = left[i] + right[i];
}
}
/// <summary>
/// Scales all elements in a span by a scalar value using SIMD.
/// result[i] = source[i] * scalar
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Scale(ReadOnlySpan<double> source, double scalar, Span<double> result)
{
if (source.Length != result.Length)
throw new ArgumentException("Source and result spans must have the same length", nameof(result));
int i = 0;
if (Vector.IsHardwareAccelerated && source.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
var scalarVec = new Vector<double>(scalar);
for (; i <= source.Length - vectorSize; i += vectorSize)
{
var vSource = new Vector<double>(source.Slice(i, vectorSize));
(vSource * scalarVec).CopyTo(result.Slice(i, vectorSize));
}
}
for (; i < source.Length; i++)
{
result[i] = source[i] * scalar;
}
}
/// <summary>
/// Element-wise subtraction of two spans using SIMD.
/// result[i] = left[i] - right[i]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Subtract(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
{
if (left.Length != right.Length || left.Length != result.Length)
throw new ArgumentException("All spans must have the same length", nameof(result));
int i = 0;
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
for (; i <= left.Length - vectorSize; i += vectorSize)
{
var vLeft = new Vector<double>(left.Slice(i, vectorSize));
var vRight = new Vector<double>(right.Slice(i, vectorSize));
(vLeft - vRight).CopyTo(result.Slice(i, vectorSize));
}
}
for (; i < left.Length; i++)
{
result[i] = left[i] - right[i];
}
}
/// <summary>
/// Calculates the dot product of two spans using SIMD intrinsics.
/// Supports AVX512, AVX2, and NEON (ARM64).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double DotProduct(this ReadOnlySpan<double> a, ReadOnlySpan<double> b)
{
if (a.Length != b.Length)
throw new ArgumentException("Spans must have equal length", nameof(b));
if (a.IsEmpty) return 0.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 (AdvSimd.Arm64.IsSupported)
return DotProductNeon(a, b);
double s1 = 0, s2 = 0, s3 = 0, s4 = 0;
ref double ar = ref MemoryMarshal.GetReference(a);
ref double br = ref MemoryMarshal.GetReference(b);
int i = 0;
// Unroll scalar loop with 4 accumulators to break dependency chains
for (; i <= len - 4; i += 4)
{
s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1);
s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2);
s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3);
}
double s = s1 + s2 + s3 + s4;
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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector512<double> vSum = Vector512<double>.Zero;
Vector512<double> vSum2 = Vector512<double>.Zero;
Vector512<double> vSum3 = Vector512<double>.Zero;
Vector512<double> vSum4 = Vector512<double>.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);
Vector256<double> v256 = Avx.Add(vSum.GetLower(), vSum.GetUpper());
Vector128<double> lower = v256.GetLower();
Vector128<double> upper = v256.GetUpper();
Vector128<double> 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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector256<double> vSum = Vector256<double>.Zero;
Vector256<double> vSum2 = Vector256<double>.Zero;
Vector256<double> vSum3 = Vector256<double>.Zero;
Vector256<double> vSum4 = Vector256<double>.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<double> lower = vSum.GetLower();
Vector128<double> upper = vSum.GetUpper();
Vector128<double> 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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector128<double> vSum = Vector128<double>.Zero;
Vector128<double> vSum2 = Vector128<double>.Zero;
Vector128<double> vSum3 = Vector128<double>.Zero;
Vector128<double> vSum4 = Vector128<double>.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;
}
}
+52
View File
@@ -0,0 +1,52 @@
# SimdExtensions Class
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
## Key Features
* **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
* **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
* **Zero-Allocation**: Operates directly on spans without creating new arrays.
* **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
## Available Methods
| Method | Description |
| ------ | ------ |
| `ContainsNonFinite()` | Checks if span contains any non-finite values (NaN or Infinity). |
| `SumSIMD()` | Calculates the sum of elements. |
| `MinSIMD()` | Finds the minimum value. |
| `MaxSIMD()` | Finds the maximum value. |
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
| `AverageSIMD()` | Calculates the arithmetic mean. |
| `VarianceSIMD()` | Calculates the sample variance. |
| `StdDevSIMD()` | Calculates the sample standard deviation. |
| `DotProduct()` | Calculates the dot product of two spans. |
## Performance
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
## Usage
```csharp
using QuanTAlib;
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
ReadOnlySpan<double> span = data;
// Calculate sum
double sum = span.SumSIMD();
// Calculate min and max in one pass
var (min, max) = span.MinMaxSIMD();
// Calculate standard deviation
double stdDev = span.StdDevSIMD();
// Check for valid data
bool hasInvalid = span.ContainsNonFinite();
// Calculate dot product
double dot = span.DotProduct(otherSpan);
```