From b3b30f69a00bef4d5711cb76ca8f29278a61f699 Mon Sep 17 00:00:00 2001
From: Uddoo <19704816+Uddoo@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:22:45 +0800
Subject: [PATCH 1/3] bench: add array equality and decoding baselines for #142
---
HdrHistogram.Benchmarking/Program.cs | 2 +
.../Utilities/ArrayEqualityBenchmark.cs | 32 +++++++++++++
.../ArrayEqualityDecodingBenchmark.cs | 46 +++++++++++++++++++
3 files changed, 80 insertions(+)
create mode 100644 HdrHistogram.Benchmarking/Utilities/ArrayEqualityBenchmark.cs
create mode 100644 HdrHistogram.Benchmarking/Utilities/ArrayEqualityDecodingBenchmark.cs
diff --git a/HdrHistogram.Benchmarking/Program.cs b/HdrHistogram.Benchmarking/Program.cs
index 881c88e..1b817e6 100644
--- a/HdrHistogram.Benchmarking/Program.cs
+++ b/HdrHistogram.Benchmarking/Program.cs
@@ -24,6 +24,8 @@ static void Main(string[] args)
typeof(LeadingZeroCount.LeadingZeroCount64BitBenchmark),
typeof(LeadingZeroCount.LeadingZeroCount32BitBenchmark),
typeof(Recording.Recording32BitBenchmark),
+ typeof(Utilities.ArrayEqualityBenchmark),
+ typeof(Utilities.ArrayEqualityDecodingBenchmark),
});
switcher.Run(args, config);
}
diff --git a/HdrHistogram.Benchmarking/Utilities/ArrayEqualityBenchmark.cs b/HdrHistogram.Benchmarking/Utilities/ArrayEqualityBenchmark.cs
new file mode 100644
index 0000000..142bd64
--- /dev/null
+++ b/HdrHistogram.Benchmarking/Utilities/ArrayEqualityBenchmark.cs
@@ -0,0 +1,32 @@
+/*
+ * This is a .NET port of the original Java version, which was written by
+ * Gil Tene as described in
+ * https://github.com/HdrHistogram/HdrHistogram
+ * and released to the public domain, as explained at
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+using BenchmarkDotNet.Attributes;
+using HdrHistogram.Utilities;
+
+namespace HdrHistogram.Benchmarking.Utilities
+{
+ ///
+ /// Measures equality checks over separate, equal long arrays.
+ ///
+ [MemoryDiagnoser]
+ public class ArrayEqualityBenchmark
+ {
+ private readonly long[] _source = new long[23552];
+ private readonly long[] _other = new long[23552];
+
+ ///
+ /// Compares approximately 184 KiB of long values.
+ ///
+ /// Whether all elements are equal.
+ [Benchmark]
+ public bool EqualLongArrays()
+ {
+ return _source.IsSequenceEqual(_other);
+ }
+ }
+}
diff --git a/HdrHistogram.Benchmarking/Utilities/ArrayEqualityDecodingBenchmark.cs b/HdrHistogram.Benchmarking/Utilities/ArrayEqualityDecodingBenchmark.cs
new file mode 100644
index 0000000..cf75124
--- /dev/null
+++ b/HdrHistogram.Benchmarking/Utilities/ArrayEqualityDecodingBenchmark.cs
@@ -0,0 +1,46 @@
+/*
+ * This is a .NET port of the original Java version, which was written by
+ * Gil Tene as described in
+ * https://github.com/HdrHistogram/HdrHistogram
+ * and released to the public domain, as explained at
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+using BenchmarkDotNet.Attributes;
+using HdrHistogram.Encoding;
+using HdrHistogram.Utilities;
+
+namespace HdrHistogram.Benchmarking.Utilities
+{
+ ///
+ /// Measures histogram decoding, whose constructor lookup compares Type arrays.
+ ///
+ [MemoryDiagnoser]
+ public class ArrayEqualityDecodingBenchmark
+ {
+ private ByteBuffer _buffer = null!;
+
+ ///
+ /// Encodes a histogram outside the measured decoding operation.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var histogram = new LongHistogram(3600000000, 3);
+ histogram.RecordValue(1000);
+ histogram.RecordValue(1000000);
+ _buffer = ByteBuffer.Allocate(histogram.GetNeededByteBufferCapacity());
+ histogram.Encode(_buffer, HistogramEncoderV2.Instance);
+ }
+
+ ///
+ /// Decodes a histogram through the public API.
+ ///
+ /// The decoded histogram.
+ [Benchmark]
+ public HistogramBase DecodeHistogram()
+ {
+ _buffer.Position = 0;
+ return HistogramEncoding.DecodeFromByteBuffer(_buffer, 0);
+ }
+ }
+}
From d19f3605c57cefc1719feaaa654705a0b82b205f Mon Sep 17 00:00:00 2001
From: Uddoo <19704816+Uddoo@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:32:16 +0800
Subject: [PATCH 2/3] fix: avoid primitive array comparison boxing without
changing custom equality
---
.../Utilities/ArrayExtensionsTests.cs | 275 ++++++++++++++++++
HdrHistogram/Utilities/ArrayExtensions.cs | 7 +
plan/benchmarks/baseline.md | 24 ++
3 files changed, 306 insertions(+)
create mode 100644 HdrHistogram.UnitTests/Utilities/ArrayExtensionsTests.cs
create mode 100644 plan/benchmarks/baseline.md
diff --git a/HdrHistogram.UnitTests/Utilities/ArrayExtensionsTests.cs b/HdrHistogram.UnitTests/Utilities/ArrayExtensionsTests.cs
new file mode 100644
index 0000000..56573d4
--- /dev/null
+++ b/HdrHistogram.UnitTests/Utilities/ArrayExtensionsTests.cs
@@ -0,0 +1,275 @@
+/*
+ * This is a .NET port of the original Java version, which was written by
+ * Gil Tene as described in
+ * https://github.com/HdrHistogram/HdrHistogram
+ * and released to the public domain, as explained at
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+using System;
+using System.Runtime.CompilerServices;
+using FluentAssertions;
+using HdrHistogram.Utilities;
+using Xunit;
+
+namespace HdrHistogram.UnitTests.Utilities
+{
+ public class ArrayExtensionsTests
+ {
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(3)]
+ [InlineData(31)]
+ [InlineData(32)]
+ [InlineData(33)]
+ [InlineData(23552)]
+ public void IsSequenceEqual_returns_true_for_equal_long_arrays(int length)
+ {
+ var source = new long[length];
+ for (int i = 0; i < length; i++)
+ {
+ source[i] = long.MaxValue - i;
+ }
+ var other = (long[])source.Clone();
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(16)]
+ [InlineData(32)]
+ public void IsSequenceEqual_returns_false_for_different_long_elements(int index)
+ {
+ var source = new long[33];
+ var other = new long[33];
+ other[index] = long.MinValue;
+
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_returns_false_for_different_lengths()
+ {
+ new long[] { 1 }.IsSequenceEqual(new long[] { 1, 2 }).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_returns_false_for_different_order()
+ {
+ new long[] { 1, 2 }.IsSequenceEqual(new long[] { 2, 1 }).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_compares_null_elements_in_reference_arrays()
+ {
+ string?[] source = { "first", null, "last" };
+ string?[] other = { "first", null, "last" };
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ other[1] = "different";
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_compares_nullable_value_elements()
+ {
+ long?[] source = { long.MinValue, null, long.MaxValue };
+ long?[] other = { long.MinValue, null, long.MaxValue };
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ other[1] = 0;
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_compares_types_used_by_constructor_lookup()
+ {
+ Type[] source = { typeof(long), typeof(long), typeof(int) };
+ Type[] other = { typeof(long), typeof(long), typeof(int) };
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ other[2] = typeof(long);
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_compares_types_without_IEquatable()
+ {
+ ObjectEqualityValue[] source = { new ObjectEqualityValue(1) };
+ ObjectEqualityValue[] other = { new ObjectEqualityValue(1) };
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_preserves_object_equality_for_equatable_classes()
+ {
+ EquatableReference[] source = { new EquatableReference() };
+ EquatableReference[] other = { new EquatableReference() };
+
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_preserves_object_equality_for_equatable_structs()
+ {
+ EquatableValue[] source = { new EquatableValue(1, 2) };
+ EquatableValue[] other = { new EquatableValue(1, 3) };
+
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_preserves_object_equality_for_nullable_structs()
+ {
+ EquatableValue?[] source = { null, new EquatableValue(1, 2) };
+ EquatableValue?[] other = { null, new EquatableValue(1, 3) };
+
+ source.IsSequenceEqual(other).Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void IsSequenceEqual_accepts_covariant_reference_arrays(int length)
+ {
+ object[] source = new string[length];
+ object[] other = new string[length];
+ if (length > 0)
+ {
+ source[0] = "same";
+ other[0] = "same";
+ }
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_compares_floating_point_values_using_equality()
+ {
+ double[] source = { double.NaN, -0.0 };
+ double[] other = { BitConverter.Int64BitsToDouble(unchecked((long)0xfff8000000000001)), 0.0 };
+ float[] floatSource = { float.NaN, -0.0f };
+ float[] floatOther = { float.NaN, 0.0f };
+
+ source.IsSequenceEqual(other).Should().BeTrue();
+ floatSource.IsSequenceEqual(floatOther).Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_rejects_null_source()
+ {
+ long[] source = null!;
+ Action compare = () => source.IsSequenceEqual(Array.Empty());
+
+ compare.Should().Throw();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_rejects_null_other()
+ {
+ long[] other = null!;
+ Action compare = () => Array.Empty().IsSequenceEqual(other);
+
+ compare.Should().Throw();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_rejects_two_null_arrays()
+ {
+ long[] source = null!;
+ long[] other = null!;
+ Action compare = () => source.IsSequenceEqual(other);
+
+ compare.Should().Throw();
+ }
+
+ [Fact]
+ public void IsSequenceEqual_does_not_allocate_for_long_elements()
+ {
+ var source = new long[1024];
+ var other = new long[1024];
+ bool equal = true;
+ for (int i = 0; i < 16; i++)
+ {
+ equal &= source.IsSequenceEqual(other);
+ }
+
+ long before = GC.GetAllocatedBytesForCurrentThread();
+ for (int i = 0; i < 16; i++)
+ {
+ equal &= source.IsSequenceEqual(other);
+ }
+ long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+
+ equal.Should().BeTrue();
+ allocated.Should().Be(0);
+ }
+
+ private sealed class ObjectEqualityValue
+ {
+ private readonly int _value;
+
+ public ObjectEqualityValue(int value)
+ {
+ _value = value;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is ObjectEqualityValue other && _value == other._value;
+ }
+
+ public override int GetHashCode()
+ {
+ return _value;
+ }
+ }
+
+ private sealed class EquatableReference : IEquatable
+ {
+ public bool Equals(EquatableReference? other)
+ {
+ return other is not null;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return ReferenceEquals(this, obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return RuntimeHelpers.GetHashCode(this);
+ }
+ }
+
+ private readonly struct EquatableValue : IEquatable
+ {
+ private readonly int _key;
+ private readonly int _detail;
+
+ public EquatableValue(int key, int detail)
+ {
+ _key = key;
+ _detail = detail;
+ }
+
+ public bool Equals(EquatableValue other)
+ {
+ return _key == other._key;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is EquatableValue other && _key == other._key && _detail == other._detail;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(_key, _detail);
+ }
+ }
+ }
+}
diff --git a/HdrHistogram/Utilities/ArrayExtensions.cs b/HdrHistogram/Utilities/ArrayExtensions.cs
index a5a9bc1..fb17658 100644
--- a/HdrHistogram/Utilities/ArrayExtensions.cs
+++ b/HdrHistogram/Utilities/ArrayExtensions.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace HdrHistogram.Utilities
{
///
@@ -17,6 +19,11 @@ public static bool IsSequenceEqual(this T[] source, T[] other)
if (source.Length != other.Length)
return false;
+ // Primitive equality agrees with object.Equals; preserve custom equality
+ // and covariant reference arrays by keeping their existing comparison path.
+ if (typeof(T).IsPrimitive)
+ return source.AsSpan().SequenceEqual(other.AsSpan(), comparer: null);
+
for (int i = 0; i < other.Length; i++)
{
if (!Equals(source[i], other[i]))
diff --git a/plan/benchmarks/baseline.md b/plan/benchmarks/baseline.md
new file mode 100644
index 0000000..6991de8
--- /dev/null
+++ b/plan/benchmarks/baseline.md
@@ -0,0 +1,24 @@
+# Array equality baseline
+
+Base library: `023f13c77134b6e15626f6eb8325a8b365f4d62e`.
+Benchmark-only commit: `b3b30f6`.
+The implementation is unchanged.
+
+BenchmarkDotNet 0.15.8, default job settings, .NET 8.0.31 x64, SDK 10.0.401, Windows 11, Intel Core i9-13900K.
+The same two benchmark classes and command are used before and after the change.
+A temporary runner calls `BenchmarkSwitcher.FromTypes(new[] { typeof(ArrayEqualityBenchmark), typeof(ArrayEqualityDecodingBenchmark) }).Run(args)` with the default configuration.
+This selects .NET 8 alone without the repository entry point adding its three explicit runtime jobs.
+
+```text
+dotnet run -c Release --project /Runner.csproj -- --filter "*ArrayEquality*" --runtimes net8.0 --exporters json --artifacts
+```
+
+| Method | Mean (ns) | StdDev (ns) | Allocated (bytes/op) | Operations/s |
+| --- | ---: | ---: | ---: | ---: |
+| EqualLongArrays | 101983.71 | 5050.88 | 1130496 | 9805.49 |
+| DecodeHistogram | 28356.44 | 764.77 | 189276 | 35265.36 |
+
+The microbenchmark compares two separate 23,552-element long arrays (184 KiB each).
+The end-to-end benchmark decodes an encoded histogram through the public API and its constructor lookup.
+That lookup compares `Type[]`, so it does not box primitive values.
+Do not infer a histogram decoding speedup from the long-array microbenchmark.
From 050803226c6d609ca8c357a4c25c43c57846b526 Mon Sep 17 00:00:00 2001
From: Uddoo <19704816+Uddoo@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:34:04 +0800
Subject: [PATCH 3/3] docs: record array equality benchmark comparison
---
plan/benchmarks/comparison.md | 30 ++++++++++++++++++++++++++++++
plan/benchmarks/post-change.md | 13 +++++++++++++
2 files changed, 43 insertions(+)
create mode 100644 plan/benchmarks/comparison.md
create mode 100644 plan/benchmarks/post-change.md
diff --git a/plan/benchmarks/comparison.md b/plan/benchmarks/comparison.md
new file mode 100644
index 0000000..f173813
--- /dev/null
+++ b/plan/benchmarks/comparison.md
@@ -0,0 +1,30 @@
+# Array equality comparison
+
+Both runs use .NET 8.0.31 on the same machine with default BenchmarkDotNet settings.
+Baseline and implementation revisions are recorded in the companion results files.
+
+| Method | Baseline mean (ns) | After mean (ns) | Delta (ns) | Delta (%) | Baseline bytes/op | After bytes/op |
+| --- | ---: | ---: | ---: | ---: | ---: | ---: |
+| EqualLongArrays | 101983.71 | 3457.48 | -98526.23 | -96.61% | 1130496 | 0 |
+| DecodeHistogram | 28356.44 | 27770.42 | -586.02 | -2.07% | 189276 | 189276 |
+
+## Interpretation
+
+The primitive long-array comparison eliminates per-element boxing and its measured allocations.
+Custom value/reference types retain the original `object.Equals` comparison.
+This preserves typed versus object equality differences, nullable values and covariant reference arrays.
+Floating-point comparisons retain equality semantics for NaN and signed zero.
+
+The existing histogram decoding caller compares `Type[]`, which does not box elements and uses the unchanged fallback.
+Its allocations are unchanged and its timing delta is within the spread of these measurements.
+No end-to-end histogram decoding improvement is claimed.
+No benchmark showed a mean regression in this run.
+Benchmarks were measured only on .NET 8; .NET 9 and .NET 10 are covered by correctness tests, not performance claims.
+
+## Regression evidence
+
+Before the implementation change, 25 compatibility cases passed and the new allocation case failed.
+It measured 786,432 bytes for sixteen comparisons of two 1,024-element long arrays, against an expected zero.
+After the change, all 26 targeted cases pass.
+The complete suite passed 925 tests on each of .NET 8, .NET 9 and .NET 10.
+All results described here are automated checks.
diff --git a/plan/benchmarks/post-change.md b/plan/benchmarks/post-change.md
new file mode 100644
index 0000000..fc9f03e
--- /dev/null
+++ b/plan/benchmarks/post-change.md
@@ -0,0 +1,13 @@
+# Array equality after the change
+
+Measured at implementation commit `d19f360`.
+Environment, temporary runner and default BenchmarkDotNet configuration match `baseline.md`.
+No ShortRun job or reduced iteration settings were used.
+
+| Method | Mean (ns) | StdDev (ns) | Allocated (bytes/op) | Operations/s |
+| --- | ---: | ---: | ---: | ---: |
+| EqualLongArrays | 3457.48 | 16.24 | 0 | 289227.91 |
+| DecodeHistogram | 27770.42 | 793.14 | 189276 | 36009.54 |
+
+BenchmarkDotNet reported a multimodal distribution for `DecodeHistogram` and removed one outlier.
+The decoding timing difference is treated as noise, not as evidence of a speedup.