mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 18:48:05 +00:00
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:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
```
|
||||
Reference in New Issue
Block a user