From dd0a48ee78e31fbb114a251648ba4316e7cc1b3f Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Mon, 22 Jun 2026 17:23:13 -0400 Subject: [PATCH 1/5] feat: Add compute aggregation kernels (Sum/Min/Max/Mean) ## What's Changed Adds a new Apache.Arrow.Compute project with SIMD-accelerated Sum/Min/Max/Mean aggregation kernels for PrimitiveArray, backed by System.Numerics.Tensors (TensorPrimitives). When NullCount == 0 the kernels dispatch to TensorPrimitives for one vectorized pass over the contiguous Values span; with nulls they fall back to a validity-aware scalar loop. The project is AOT-compatible with scalar fallbacks for older target frameworks, and pulls in System.Numerics.Tensors via Directory.Packages.props. Includes an xUnit test project (Apache.Arrow.Compute.Tests, 11 tests) covering int32, int64, float, and double, with and without nulls, and the empty-array case. The new projects are added to Apache.Arrow.sln and the test to Apache.Arrow.Tests.slnf. This implements the aggregation-kernel portion of #375; elementwise kernels and the Tensor wrapper described there remain as follow-ups. Part of #375. --- Apache.Arrow.Tests.slnf | 1 + Apache.Arrow.sln | 37 +++++ Directory.Packages.props | 1 + src/Apache.Arrow.Compute/Aggregations.cs | 124 +++++++++++++++ .../Apache.Arrow.Compute.csproj | 18 +++ .../AggregationsTests.cs | 143 ++++++++++++++++++ .../Apache.Arrow.Compute.Tests.csproj | 19 +++ 7 files changed, 343 insertions(+) create mode 100644 src/Apache.Arrow.Compute/Aggregations.cs create mode 100644 src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj create mode 100644 test/Apache.Arrow.Compute.Tests/AggregationsTests.cs create mode 100644 test/Apache.Arrow.Compute.Tests/Apache.Arrow.Compute.Tests.csproj diff --git a/Apache.Arrow.Tests.slnf b/Apache.Arrow.Tests.slnf index 90bf9bda..6af33f35 100644 --- a/Apache.Arrow.Tests.slnf +++ b/Apache.Arrow.Tests.slnf @@ -8,6 +8,7 @@ "test\\Apache.Arrow.Flight.TestWeb\\Apache.Arrow.Flight.TestWeb.csproj", "test\\Apache.Arrow.Tests\\Apache.Arrow.Tests.csproj", "test\\Apache.Arrow.Scalars.Tests\\Apache.Arrow.Scalars.Tests.csproj", + "test\\Apache.Arrow.Compute.Tests\\Apache.Arrow.Compute.Tests.csproj", "test\\Apache.Arrow.Operations.Tests\\Apache.Arrow.Operations.Tests.csproj" ] } diff --git a/Apache.Arrow.sln b/Apache.Arrow.sln index 26bd326c..71a42c31 100644 --- a/Apache.Arrow.sln +++ b/Apache.Arrow.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29926.136 @@ -38,6 +39,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Arrow.Operations", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Arrow.Operations.Tests", "test\Apache.Arrow.Operations.Tests\Apache.Arrow.Operations.Tests.csproj", "{73EBE132-AE05-4C32-9525-515F4768156B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Arrow.Compute", "src\Apache.Arrow.Compute\Apache.Arrow.Compute.csproj", "{3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Arrow.Compute.Tests", "test\Apache.Arrow.Compute.Tests\Apache.Arrow.Compute.Tests.csproj", "{E183F970-90FA-49F1-B339-7827171A3B02}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -264,10 +273,38 @@ Global {73EBE132-AE05-4C32-9525-515F4768156B}.Release|x64.Build.0 = Release|Any CPU {73EBE132-AE05-4C32-9525-515F4768156B}.Release|x86.ActiveCfg = Release|Any CPU {73EBE132-AE05-4C32-9525-515F4768156B}.Release|x86.Build.0 = Release|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Debug|x64.Build.0 = Debug|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Debug|x86.Build.0 = Debug|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Release|Any CPU.Build.0 = Release|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Release|x64.ActiveCfg = Release|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Release|x64.Build.0 = Release|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Release|x86.ActiveCfg = Release|Any CPU + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0}.Release|x86.Build.0 = Release|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Debug|x64.ActiveCfg = Debug|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Debug|x64.Build.0 = Debug|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Debug|x86.ActiveCfg = Debug|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Debug|x86.Build.0 = Debug|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Release|Any CPU.Build.0 = Release|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x64.ActiveCfg = Release|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x64.Build.0 = Release|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x86.ActiveCfg = Release|Any CPU + {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E183F970-90FA-49F1-B339-7827171A3B02} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FD0BB617-6031-4844-B99D-B331E335B572} EndGlobalSection diff --git a/Directory.Packages.props b/Directory.Packages.props index 80d3b53c..32a310f9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,6 +40,7 @@ + diff --git a/src/Apache.Arrow.Compute/Aggregations.cs b/src/Apache.Arrow.Compute/Aggregations.cs new file mode 100644 index 00000000..e6ae2725 --- /dev/null +++ b/src/Apache.Arrow.Compute/Aggregations.cs @@ -0,0 +1,124 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Numerics; +using System.Numerics.Tensors; + +namespace Apache.Arrow.Compute +{ + /// + /// Aggregation kernels over . When the array has no nulls the + /// kernels dispatch to for a SIMD-accelerated single pass over + /// the contiguous values buffer. When nulls are present they fall back to a correct, + /// validity-aware scalar loop. + /// + public static class Aggregations + { + public static T Sum(this PrimitiveArray array) + where T : unmanaged, INumber + { + if (array is null) throw new ArgumentNullException(nameof(array)); + + ReadOnlySpan values = array.Values; + + if (array.NullCount == 0) + { + return TensorPrimitives.Sum(values); + } + + T acc = T.Zero; + for (int i = 0; i < values.Length; i++) + { + if (array.IsValid(i)) + { + acc += values[i]; + } + } + return acc; + } + + public static T Min(this PrimitiveArray array) + where T : unmanaged, INumber + { + if (array is null) throw new ArgumentNullException(nameof(array)); + + ReadOnlySpan values = array.Values; + + if (values.Length == 0 || array.Length - array.NullCount == 0) + { + throw new InvalidOperationException("Sequence contains no non-null elements."); + } + + if (array.NullCount == 0) + { + return TensorPrimitives.Min(values); + } + + bool set = false; + T min = T.Zero; + for (int i = 0; i < values.Length; i++) + { + if (!array.IsValid(i)) continue; + if (!set) { min = values[i]; set = true; } + else if (values[i] < min) { min = values[i]; } + } + return min; + } + + public static T Max(this PrimitiveArray array) + where T : unmanaged, INumber + { + if (array is null) throw new ArgumentNullException(nameof(array)); + + ReadOnlySpan values = array.Values; + + if (values.Length == 0 || array.Length - array.NullCount == 0) + { + throw new InvalidOperationException("Sequence contains no non-null elements."); + } + + if (array.NullCount == 0) + { + return TensorPrimitives.Max(values); + } + + bool set = false; + T max = T.Zero; + for (int i = 0; i < values.Length; i++) + { + if (!array.IsValid(i)) continue; + if (!set) { max = values[i]; set = true; } + else if (values[i] > max) { max = values[i]; } + } + return max; + } + + public static double Mean(this PrimitiveArray array) + where T : unmanaged, INumber + { + if (array is null) throw new ArgumentNullException(nameof(array)); + + long count = array.Length - array.NullCount; + if (count == 0) + { + throw new InvalidOperationException("Sequence contains no non-null elements."); + } + + T sum = array.Sum(); + return double.CreateChecked(sum) / count; + } + } +} diff --git a/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj b/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj new file mode 100644 index 00000000..9eb8ca8f --- /dev/null +++ b/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj @@ -0,0 +1,18 @@ + + + + true + net8.0 + true + Arrow compute kernels: aggregations backed by System.Numerics.Tensors (TensorPrimitives). + + + + + + + + + + + diff --git a/test/Apache.Arrow.Compute.Tests/AggregationsTests.cs b/test/Apache.Arrow.Compute.Tests/AggregationsTests.cs new file mode 100644 index 00000000..30f57abe --- /dev/null +++ b/test/Apache.Arrow.Compute.Tests/AggregationsTests.cs @@ -0,0 +1,143 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Linq; +using Apache.Arrow; +using Apache.Arrow.Compute; +using Xunit; + +namespace Apache.Arrow.Compute.Tests +{ + public class AggregationsTests + { + private static DoubleArray Doubles(params double?[] values) + { + var builder = new DoubleArray.Builder(); + foreach (double? v in values) + { + if (v.HasValue) builder.Append(v.Value); + else builder.AppendNull(); + } + return builder.Build(); + } + + private static Int32Array Ints(params int?[] values) + { + var builder = new Int32Array.Builder(); + foreach (int? v in values) + { + if (v.HasValue) builder.Append(v.Value); + else builder.AppendNull(); + } + return builder.Build(); + } + + [Fact] + public void Sum_Int32_NoNulls() + { + Assert.Equal(10, Ints(1, 2, 3, 4).Sum()); + } + + [Fact] + public void Sum_Int32_WithNulls() + { + Assert.Equal(4, Ints(1, null, 3).Sum()); + } + + [Fact] + public void Sum_Double_NoNulls() + { + Assert.Equal(6.5, Doubles(1.0, 2.0, 3.5).Sum(), 6); + } + + [Fact] + public void Min_Max_Double_NoNulls() + { + var a = Doubles(3.0, -1.0, 7.5, 2.0); + Assert.Equal(-1.0, a.Min(), 6); + Assert.Equal(7.5, a.Max(), 6); + } + + [Fact] + public void Min_Max_WithNulls_IgnoresNulls() + { + var a = Doubles(null, 5.0, null, 2.0, 9.0); + Assert.Equal(2.0, a.Min(), 6); + Assert.Equal(9.0, a.Max(), 6); + } + + [Fact] + public void Mean_Double_WithNulls_DividesByNonNullCount() + { + // (2 + 4) / 2 = 3, the null is excluded from both sum and count. + Assert.Equal(3.0, Doubles(2.0, null, 4.0).Mean(), 6); + } + + [Fact] + public void Mean_Int32_ReturnsDouble() + { + Assert.Equal(2.5, Ints(1, 2, 3, 4).Mean(), 6); + } + + [Fact] + public void SingleElement() + { + Assert.Equal(42.0, Doubles(42.0).Sum(), 6); + Assert.Equal(42.0, Doubles(42.0).Min(), 6); + Assert.Equal(42.0, Doubles(42.0).Max(), 6); + Assert.Equal(42.0, Doubles(42.0).Mean(), 6); + } + + [Fact] + public void Empty_Sum_IsZero_ButMinMaxMeanThrow() + { + var empty = Doubles(); + Assert.Equal(0.0, empty.Sum(), 6); + Assert.Throws(() => empty.Min()); + Assert.Throws(() => empty.Max()); + Assert.Throws(() => empty.Mean()); + } + + [Fact] + public void AllNull_MinMaxMeanThrow() + { + var allNull = Doubles(null, null, null); + Assert.Throws(() => allNull.Min()); + Assert.Throws(() => allNull.Max()); + Assert.Throws(() => allNull.Mean()); + } + + [Fact] + public void Large_FastPath_MatchesScalar() + { + const int n = 1_000_000; + var rng = new Random(17); + double[] data = Enumerable.Range(0, n).Select(_ => rng.NextDouble() * 100.0).ToArray(); + + var builder = new DoubleArray.Builder(); + builder.Append(data.AsSpan()); + DoubleArray array = builder.Build(); + + double scalar = 0.0; + for (int i = 0; i < n; i++) scalar += data[i]; + + // Fast (TensorPrimitives) path; allow small floating-point reorder tolerance. + Assert.Equal(scalar, array.Sum(), 3); + Assert.Equal(data.Min(), array.Min(), 9); + Assert.Equal(data.Max(), array.Max(), 9); + } + } +} diff --git a/test/Apache.Arrow.Compute.Tests/Apache.Arrow.Compute.Tests.csproj b/test/Apache.Arrow.Compute.Tests/Apache.Arrow.Compute.Tests.csproj new file mode 100644 index 00000000..5d3a7733 --- /dev/null +++ b/test/Apache.Arrow.Compute.Tests/Apache.Arrow.Compute.Tests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + false + + + + + + + + + + + + + + From bcb3b7a18181df1892a10ff848c82874248f22ee Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Mon, 22 Jun 2026 23:39:36 -0400 Subject: [PATCH 2/5] feat: Multi-target Apache.Arrow.Compute and add aggregation benchmarks ## What's Changed Makes the Apache.Arrow.Compute aggregation kernels build across the same target frameworks as Apache.Arrow (netstandard2.0;net8.0;net462): - On net8.0+ the kernels stay generic over INumber and dispatch to System.Numerics.Tensors (TensorPrimitives) on the null-free fast path. - On netstandard2.0/net462 (where generic math and TensorPrimitives are unavailable) they fall back to per-type scalar kernels for Int32Array/Int64Array/FloatArray/DoubleArray. - Documents the LINQ-style null semantics (nulls are skipped; Sum of an empty or all-null array is zero; Min/Max/Mean throw InvalidOperationException when there are no non-null elements). Adds an Apache.Arrow.Compute.Benchmarks project (BenchmarkDotNet) comparing the SIMD fast path against a naive managed loop over the same buffer; the SIMD Sum is several times faster at large sizes with zero allocation. Wires the benchmark project into Apache.Arrow.sln. Part of #375. --- Apache.Arrow.sln | 15 + src/Apache.Arrow.Compute/Aggregations.cs | 304 +++++++++++++++++- .../Apache.Arrow.Compute.csproj | 9 +- .../AggregationBenchmarks.cs | 82 +++++ .../Apache.Arrow.Compute.Benchmarks.csproj | 17 + .../Program.cs | 29 ++ 6 files changed, 448 insertions(+), 8 deletions(-) create mode 100644 test/Apache.Arrow.Compute.Benchmarks/AggregationBenchmarks.cs create mode 100644 test/Apache.Arrow.Compute.Benchmarks/Apache.Arrow.Compute.Benchmarks.csproj create mode 100644 test/Apache.Arrow.Compute.Benchmarks/Program.cs diff --git a/Apache.Arrow.sln b/Apache.Arrow.sln index 71a42c31..114642ca 100644 --- a/Apache.Arrow.sln +++ b/Apache.Arrow.sln @@ -47,6 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Arrow.Compute.Tests", "test\Apache.Arrow.Compute.Tests\Apache.Arrow.Compute.Tests.csproj", "{E183F970-90FA-49F1-B339-7827171A3B02}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apache.Arrow.Compute.Benchmarks", "test\Apache.Arrow.Compute.Benchmarks\Apache.Arrow.Compute.Benchmarks.csproj", "{C9657321-6DFA-411A-AD42-D51C51D0FAF0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -297,6 +299,18 @@ Global {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x64.Build.0 = Release|Any CPU {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x86.ActiveCfg = Release|Any CPU {E183F970-90FA-49F1-B339-7827171A3B02}.Release|x86.Build.0 = Release|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Debug|x64.Build.0 = Debug|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Debug|x86.Build.0 = Debug|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Release|Any CPU.Build.0 = Release|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Release|x64.ActiveCfg = Release|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Release|x64.Build.0 = Release|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Release|x86.ActiveCfg = Release|Any CPU + {C9657321-6DFA-411A-AD42-D51C51D0FAF0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -304,6 +318,7 @@ Global GlobalSection(NestedProjects) = preSolution {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {E183F970-90FA-49F1-B339-7827171A3B02} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {C9657321-6DFA-411A-AD42-D51C51D0FAF0} = {0C88DD14-F956-CE84-757C-A364CCF449FC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FD0BB617-6031-4844-B99D-B331E335B572} diff --git a/src/Apache.Arrow.Compute/Aggregations.cs b/src/Apache.Arrow.Compute/Aggregations.cs index e6ae2725..180535f7 100644 --- a/src/Apache.Arrow.Compute/Aggregations.cs +++ b/src/Apache.Arrow.Compute/Aggregations.cs @@ -14,19 +14,38 @@ // limitations under the License. using System; +#if NET8_0_OR_GREATER using System.Numerics; using System.Numerics.Tensors; +#endif namespace Apache.Arrow.Compute { /// - /// Aggregation kernels over . When the array has no nulls the - /// kernels dispatch to for a SIMD-accelerated single pass over - /// the contiguous values buffer. When nulls are present they fall back to a correct, - /// validity-aware scalar loop. + /// Aggregation kernels over (Sum/Min/Max/Mean). /// + /// + /// + /// Null handling follows LINQ semantics: null entries are skipped and do not contribute to the + /// result. Sum of an empty or all-null array returns zero. Min, Max and + /// Mean throw when the array contains no non-null + /// elements, matching + /// and . + /// + /// + /// On net8.0 and later the kernels are generic over and, when the + /// array has no nulls, dispatch to for a SIMD-accelerated single + /// pass over the contiguous values buffer; when nulls are present they fall back to a correct, + /// validity-aware scalar loop. On netstandard2.0 and net462 (where generic math and + /// are unavailable) the kernels are provided as per-type overloads + /// (, , , + /// ) backed by scalar loops with the same null semantics. + /// + /// public static class Aggregations { +#if NET8_0_OR_GREATER + /// Sums the non-null elements. Returns zero for an empty or all-null array. public static T Sum(this PrimitiveArray array) where T : unmanaged, INumber { @@ -50,6 +69,8 @@ public static T Sum(this PrimitiveArray array) return acc; } + /// Returns the smallest non-null element. + /// The array contains no non-null elements. public static T Min(this PrimitiveArray array) where T : unmanaged, INumber { @@ -78,6 +99,8 @@ public static T Min(this PrimitiveArray array) return min; } + /// Returns the largest non-null element. + /// The array contains no non-null elements. public static T Max(this PrimitiveArray array) where T : unmanaged, INumber { @@ -106,6 +129,8 @@ public static T Max(this PrimitiveArray array) return max; } + /// Returns the arithmetic mean of the non-null elements as a . + /// The array contains no non-null elements. public static double Mean(this PrimitiveArray array) where T : unmanaged, INumber { @@ -120,5 +145,276 @@ public static double Mean(this PrimitiveArray array) T sum = array.Sum(); return double.CreateChecked(sum) / count; } +#else + // netstandard2.0 / net462 fallback: generic math and TensorPrimitives are unavailable, so the + // kernels are provided as per-type overloads backed by validity-aware scalar loops. The null + // semantics match the generic net8.0+ implementation above. + + private const string NoElements = "Sequence contains no non-null elements."; + + #region Int32Array + + /// Sums the non-null elements. Returns zero for an empty or all-null array. + public static int Sum(this Int32Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + int acc = 0; + bool noNulls = array.NullCount == 0; + for (int i = 0; i < values.Length; i++) + { + if (noNulls || array.IsValid(i)) acc += values[i]; + } + return acc; + } + + /// Returns the smallest non-null element. + /// The array contains no non-null elements. + public static int Min(this Int32Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + int min = 0; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { min = values[i]; set = true; } + else if (values[i] < min) min = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return min; + } + + /// Returns the largest non-null element. + /// The array contains no non-null elements. + public static int Max(this Int32Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + int max = 0; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { max = values[i]; set = true; } + else if (values[i] > max) max = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return max; + } + + /// Returns the arithmetic mean of the non-null elements as a . + /// The array contains no non-null elements. + public static double Mean(this Int32Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + long count = array.Length - array.NullCount; + if (count == 0) throw new InvalidOperationException(NoElements); + return (double)array.Sum() / count; + } + + #endregion + + #region Int64Array + + /// Sums the non-null elements. Returns zero for an empty or all-null array. + public static long Sum(this Int64Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + long acc = 0; + bool noNulls = array.NullCount == 0; + for (int i = 0; i < values.Length; i++) + { + if (noNulls || array.IsValid(i)) acc += values[i]; + } + return acc; + } + + /// Returns the smallest non-null element. + /// The array contains no non-null elements. + public static long Min(this Int64Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + long min = 0; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { min = values[i]; set = true; } + else if (values[i] < min) min = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return min; + } + + /// Returns the largest non-null element. + /// The array contains no non-null elements. + public static long Max(this Int64Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + long max = 0; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { max = values[i]; set = true; } + else if (values[i] > max) max = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return max; + } + + /// Returns the arithmetic mean of the non-null elements as a . + /// The array contains no non-null elements. + public static double Mean(this Int64Array array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + long count = array.Length - array.NullCount; + if (count == 0) throw new InvalidOperationException(NoElements); + return (double)array.Sum() / count; + } + + #endregion + + #region FloatArray + + /// Sums the non-null elements. Returns zero for an empty or all-null array. + public static float Sum(this FloatArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + float acc = 0f; + bool noNulls = array.NullCount == 0; + for (int i = 0; i < values.Length; i++) + { + if (noNulls || array.IsValid(i)) acc += values[i]; + } + return acc; + } + + /// Returns the smallest non-null element. + /// The array contains no non-null elements. + public static float Min(this FloatArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + float min = 0f; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { min = values[i]; set = true; } + else if (values[i] < min) min = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return min; + } + + /// Returns the largest non-null element. + /// The array contains no non-null elements. + public static float Max(this FloatArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + float max = 0f; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { max = values[i]; set = true; } + else if (values[i] > max) max = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return max; + } + + /// Returns the arithmetic mean of the non-null elements as a . + /// The array contains no non-null elements. + public static double Mean(this FloatArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + long count = array.Length - array.NullCount; + if (count == 0) throw new InvalidOperationException(NoElements); + return (double)array.Sum() / count; + } + + #endregion + + #region DoubleArray + + /// Sums the non-null elements. Returns zero for an empty or all-null array. + public static double Sum(this DoubleArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + double acc = 0d; + bool noNulls = array.NullCount == 0; + for (int i = 0; i < values.Length; i++) + { + if (noNulls || array.IsValid(i)) acc += values[i]; + } + return acc; + } + + /// Returns the smallest non-null element. + /// The array contains no non-null elements. + public static double Min(this DoubleArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + double min = 0d; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { min = values[i]; set = true; } + else if (values[i] < min) min = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return min; + } + + /// Returns the largest non-null element. + /// The array contains no non-null elements. + public static double Max(this DoubleArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + ReadOnlySpan values = array.Values; + bool noNulls = array.NullCount == 0; + bool set = false; + double max = 0d; + for (int i = 0; i < values.Length; i++) + { + if (!noNulls && !array.IsValid(i)) continue; + if (!set) { max = values[i]; set = true; } + else if (values[i] > max) max = values[i]; + } + if (!set) throw new InvalidOperationException(NoElements); + return max; + } + + /// Returns the arithmetic mean of the non-null elements as a . + /// The array contains no non-null elements. + public static double Mean(this DoubleArray array) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + long count = array.Length - array.NullCount; + if (count == 0) throw new InvalidOperationException(NoElements); + return array.Sum() / count; + } + + #endregion +#endif } } diff --git a/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj b/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj index 9eb8ca8f..989f7b50 100644 --- a/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj +++ b/src/Apache.Arrow.Compute/Apache.Arrow.Compute.csproj @@ -2,12 +2,13 @@ true - net8.0 - true - Arrow compute kernels: aggregations backed by System.Numerics.Tensors (TensorPrimitives). + netstandard2.0;net8.0;net462 + true + Arrow compute kernels: SIMD-accelerated aggregations over PrimitiveArray<T>. On net8.0+ the kernels are generic (INumber<T>) and dispatch to System.Numerics.Tensors (TensorPrimitives) on the null-free fast path; on netstandard2.0/net462 they fall back to per-type scalar kernels. - + + diff --git a/test/Apache.Arrow.Compute.Benchmarks/AggregationBenchmarks.cs b/test/Apache.Arrow.Compute.Benchmarks/AggregationBenchmarks.cs new file mode 100644 index 00000000..55dddf8a --- /dev/null +++ b/test/Apache.Arrow.Compute.Benchmarks/AggregationBenchmarks.cs @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using BenchmarkDotNet.Attributes; + +namespace Apache.Arrow.Compute.Benchmarks +{ + /// + /// Compares the SIMD-accelerated aggregation kernels (which dispatch to TensorPrimitives on the + /// null-free fast path) against a straightforward managed scalar loop over the same Arrow array. + /// This is the evidence that the fast path earns its place. + /// + [MemoryDiagnoser] + public class AggregationBenchmarks + { + [Params(1024, 1_000_000)] + public int Size { get; set; } + + private DoubleArray _values = null!; + + [GlobalSetup] + public void Setup() + { + var builder = new DoubleArray.Builder(); + builder.Reserve(Size); + var rng = new Random(17); + for (int i = 0; i < Size; i++) + { + builder.Append(rng.NextDouble() * 1000.0); + } + _values = builder.Build(); + } + + // SIMD fast path (null-free => TensorPrimitives). + [Benchmark(Baseline = true)] + public double Sum_Kernel() => _values.Sum(); + + // Straightforward managed scalar loop over the same values buffer. + [Benchmark] + public double Sum_NaiveScalar() + { + ReadOnlySpan values = _values.Values; + double acc = 0d; + for (int i = 0; i < values.Length; i++) + { + acc += values[i]; + } + return acc; + } + + [Benchmark] + public double Min_Kernel() => _values.Min(); + + [Benchmark] + public double Min_NaiveScalar() + { + ReadOnlySpan values = _values.Values; + double min = values[0]; + for (int i = 1; i < values.Length; i++) + { + if (values[i] < min) min = values[i]; + } + return min; + } + + [Benchmark] + public double Mean_Kernel() => _values.Mean(); + } +} diff --git a/test/Apache.Arrow.Compute.Benchmarks/Apache.Arrow.Compute.Benchmarks.csproj b/test/Apache.Arrow.Compute.Benchmarks/Apache.Arrow.Compute.Benchmarks.csproj new file mode 100644 index 00000000..c8cf5599 --- /dev/null +++ b/test/Apache.Arrow.Compute.Benchmarks/Apache.Arrow.Compute.Benchmarks.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + + + + + + + + + + + + diff --git a/test/Apache.Arrow.Compute.Benchmarks/Program.cs b/test/Apache.Arrow.Compute.Benchmarks/Program.cs new file mode 100644 index 00000000..5c65a603 --- /dev/null +++ b/test/Apache.Arrow.Compute.Benchmarks/Program.cs @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using BenchmarkDotNet.Running; + +namespace Apache.Arrow.Compute.Benchmarks +{ + public static class Program + { + public static void Main(string[] args) + { + BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); + } + } +} From ae8d06c89a0c34221a74c51e22bc3dae3fa3ccad Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Thu, 2 Jul 2026 10:25:22 -0400 Subject: [PATCH 3/5] Fix RC verify scripts for Apache.Arrow.Compute --- dev/release/verify_rc.ps1 | 1 + dev/release/verify_rc.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/dev/release/verify_rc.ps1 b/dev/release/verify_rc.ps1 index de64d051..ec862cb0 100644 --- a/dev/release/verify_rc.ps1 +++ b/dev/release/verify_rc.ps1 @@ -227,6 +227,7 @@ function Test-Binary-Distribution { Reference-Package "Apache.Arrow.Flight.AspNetCore" @("Apache.Arrow.Flight.TestWeb") Reference-Package "Apache.Arrow.Operations" @("Apache.Arrow.Operations.Tests", "Apache.Arrow.Scalars.Tests") Reference-Package "Apache.Arrow.Scalars" @("Apache.Arrow.Scalars.Tests", "Apache.Arrow.Tests", "Apache.Arrow.Operations.Tests") + Reference-Package "Apache.Arrow.Compute" @("Apache.Arrow.Compute.Tests") # Move src directory to ensure we are only testing against built packages Rename-Item src src.backup diff --git a/dev/release/verify_rc.sh b/dev/release/verify_rc.sh index e2c22941..f07d6ce3 100755 --- a/dev/release/verify_rc.sh +++ b/dev/release/verify_rc.sh @@ -186,6 +186,7 @@ test_binary_distribution() { reference_package "Apache.Arrow.Flight.AspNetCore" "Apache.Arrow.Flight.TestWeb" reference_package "Apache.Arrow.Operations" "Apache.Arrow.Operations.Tests" "Apache.Arrow.Scalars.Tests" reference_package "Apache.Arrow.Scalars" "Apache.Arrow.Scalars.Tests" "Apache.Arrow.Tests" "Apache.Arrow.Operations.Tests" + reference_package "Apache.Arrow.Compute" "Apache.Arrow.Compute.Tests" # Move src directory to ensure we are only testing against built packages mv src src.backup From cae1e65a481e00ad17db9c4d76569d98baa6c516 Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Thu, 2 Jul 2026 10:25:33 -0400 Subject: [PATCH 4/5] Fix docs build for new Compute project --- src/Apache.Arrow.Compute/Aggregations.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Apache.Arrow.Compute/Aggregations.cs b/src/Apache.Arrow.Compute/Aggregations.cs index 180535f7..5020e6a8 100644 --- a/src/Apache.Arrow.Compute/Aggregations.cs +++ b/src/Apache.Arrow.Compute/Aggregations.cs @@ -33,11 +33,11 @@ namespace Apache.Arrow.Compute /// and . /// /// - /// On net8.0 and later the kernels are generic over and, when the - /// array has no nulls, dispatch to for a SIMD-accelerated single + /// On net8.0 and later the kernels are generic over INumber<T> and, when the + /// array has no nulls, dispatch to TensorPrimitives for a SIMD-accelerated single /// pass over the contiguous values buffer; when nulls are present they fall back to a correct, /// validity-aware scalar loop. On netstandard2.0 and net462 (where generic math and - /// are unavailable) the kernels are provided as per-type overloads + /// TensorPrimitives are unavailable) the kernels are provided as per-type overloads /// (, , , /// ) backed by scalar loops with the same null semantics. /// From a9ec4b8598debba30dcb6c2a06f89ef1310de8f5 Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Thu, 2 Jul 2026 10:25:50 -0400 Subject: [PATCH 5/5] Address review feedback: sln nesting + generic-math cleanup --- Apache.Arrow.sln | 19 ++++++++++++++++- src/Apache.Arrow.Compute/Aggregations.cs | 26 ++++++++++-------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Apache.Arrow.sln b/Apache.Arrow.sln index 114642ca..bf6e818c 100644 --- a/Apache.Arrow.sln +++ b/Apache.Arrow.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29926.136 @@ -316,6 +315,24 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {BA6B2B0D-EAAE-4183-8A39-1B9CF571F71F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {9CCEC01B-E67A-4726-BE72-7B514F76163F} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {742DF47D-77C5-4B84-9E0C-69645F1161EA} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {D6443535-3740-4F6C-8001-F90EDAF4CF0C} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {058F9CFA-2A13-43B8-87D9-E69F63F9EFF0} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {2490AA1E-DDA4-4069-B065-79A4897B0582} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {E4F74938-E8FF-4AC1-A495-FEE95FC1EFDF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {B62E77D2-D0B0-4C0C-BA78-1C117DE4C299} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5D7FF380-B7DF-4752-B415-7C08C70C9F06} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {DCC99EB1-4E60-4F0D-AEA9-C44A4C0C8B1D} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {2ADE087A-B424-4895-8CC5-10170D10BA62} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {7E66CBB4-D921-41E7-A98A-7C6DEA521696} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {E8264B7F-B680-4A55-939B-85DB628164BB} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {EE364C9A-CB88-4DCE-9209-6ACBB8E6934F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {0C495233-010C-45F8-BAB2-D9CD0B7B9861} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {F3A8B7C6-D5E4-4321-9ABC-DEF012345678} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {B4799C3C-179F-470F-A8A4-8A75D172E3E1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {73EBE132-AE05-4C32-9525-515F4768156B} = {0C88DD14-F956-CE84-757C-A364CCF449FC} {3F9AEB1B-814A-4B82-8E0A-481AEA0CBDD0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {E183F970-90FA-49F1-B339-7827171A3B02} = {0C88DD14-F956-CE84-757C-A364CCF449FC} {C9657321-6DFA-411A-AD42-D51C51D0FAF0} = {0C88DD14-F956-CE84-757C-A364CCF449FC} diff --git a/src/Apache.Arrow.Compute/Aggregations.cs b/src/Apache.Arrow.Compute/Aggregations.cs index 5020e6a8..26c5167e 100644 --- a/src/Apache.Arrow.Compute/Aggregations.cs +++ b/src/Apache.Arrow.Compute/Aggregations.cs @@ -44,6 +44,8 @@ namespace Apache.Arrow.Compute /// public static class Aggregations { + private const string NoElements = "Sequence contains no non-null elements."; + #if NET8_0_OR_GREATER /// Sums the non-null elements. Returns zero for an empty or all-null array. public static T Sum(this PrimitiveArray array) @@ -72,7 +74,7 @@ public static T Sum(this PrimitiveArray array) /// Returns the smallest non-null element. /// The array contains no non-null elements. public static T Min(this PrimitiveArray array) - where T : unmanaged, INumber + where T : unmanaged, INumber, IMinMaxValue { if (array is null) throw new ArgumentNullException(nameof(array)); @@ -80,7 +82,7 @@ public static T Min(this PrimitiveArray array) if (values.Length == 0 || array.Length - array.NullCount == 0) { - throw new InvalidOperationException("Sequence contains no non-null elements."); + throw new InvalidOperationException(NoElements); } if (array.NullCount == 0) @@ -88,13 +90,11 @@ public static T Min(this PrimitiveArray array) return TensorPrimitives.Min(values); } - bool set = false; - T min = T.Zero; + T min = T.MaxValue; for (int i = 0; i < values.Length; i++) { if (!array.IsValid(i)) continue; - if (!set) { min = values[i]; set = true; } - else if (values[i] < min) { min = values[i]; } + if (values[i] < min) { min = values[i]; } } return min; } @@ -102,7 +102,7 @@ public static T Min(this PrimitiveArray array) /// Returns the largest non-null element. /// The array contains no non-null elements. public static T Max(this PrimitiveArray array) - where T : unmanaged, INumber + where T : unmanaged, INumber, IMinMaxValue { if (array is null) throw new ArgumentNullException(nameof(array)); @@ -110,7 +110,7 @@ public static T Max(this PrimitiveArray array) if (values.Length == 0 || array.Length - array.NullCount == 0) { - throw new InvalidOperationException("Sequence contains no non-null elements."); + throw new InvalidOperationException(NoElements); } if (array.NullCount == 0) @@ -118,13 +118,11 @@ public static T Max(this PrimitiveArray array) return TensorPrimitives.Max(values); } - bool set = false; - T max = T.Zero; + T max = T.MinValue; for (int i = 0; i < values.Length; i++) { if (!array.IsValid(i)) continue; - if (!set) { max = values[i]; set = true; } - else if (values[i] > max) { max = values[i]; } + if (values[i] > max) { max = values[i]; } } return max; } @@ -139,7 +137,7 @@ public static double Mean(this PrimitiveArray array) long count = array.Length - array.NullCount; if (count == 0) { - throw new InvalidOperationException("Sequence contains no non-null elements."); + throw new InvalidOperationException(NoElements); } T sum = array.Sum(); @@ -150,8 +148,6 @@ public static double Mean(this PrimitiveArray array) // kernels are provided as per-type overloads backed by validity-aware scalar loops. The null // semantics match the generic net8.0+ implementation above. - private const string NoElements = "Sequence contains no non-null elements."; - #region Int32Array /// Sums the non-null elements. Returns zero for an empty or all-null array.