This is an automated email from the ASF dual-hosted git repository.
CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-dotnet.git
The following commit(s) were added to refs/heads/main by this push:
new b739fda perf: Copy fixed-size binary values using CopyTo (#434)
b739fda is described below
commit b739fda203ce7ec380b44e9d2d84341aee1be83b
Author: George Vanburgh <[email protected]>
AuthorDate: Mon Sep 14 00:48:30 2026 +0100
perf: Copy fixed-size binary values using CopyTo (#434)
When looking at some work related to #96, I noticed that
`FixedSizeBinaryArray.BuilderBase.Set` copied each value one byte at a
time. `CopyTo` copies the requested fixed-width slice in one call to
remove work that scaled with the byte width. It's a small absolute
improvement, but it does show up on the hot path for some fixed width
type conversions.
Benchmarked the impact on `GuidArray`, and setting raw bytes. Results
are mean time per operation; lower is better.
| Operation | .NET 8 before | .NET 8 after | Change | .NET 10 before |
.NET 10 after | Change |
|---|---:|---:|---:|---:|---:|---:|
| Set GUIDs | 328.6 µs | 170.5 µs | 48% faster | 367.2 µs | 152.4 µs |
58% faster |
| Set precomputed 16-byte values | 259.3 µs | 79.3 µs | 69% faster |
318.2 µs | 62.8 µs | 80% faster |
Both runtime comparisons used BenchmarkDotNet 0.15.8, 15 measured
iterations across two launches, on an AMD Ryzen 7 5800X. Runtime
versions were .NET 8.0.31 and .NET 10.0.12. The harness verifies GUID
round trips in setup.
<details>
<summary>Benchmark harness used for these GUID results:</summary>
```csharp
using System;
using BenchmarkDotNet.Attributes;
namespace Apache.Arrow.Benchmarks
{
[MemoryDiagnoser]
public class GuidBenchmark
{
[Params(10_000)]
public int Count { get; set; }
private Guid[] _values;
private byte[][] _bytes;
private GuidArray.Builder _setBuilder;
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
_values = new Guid[Count];
_bytes = new byte[Count][];
_setBuilder = new GuidArray.Builder().Resize(Count);
_appendBuilder = new GuidArray.Builder().Reserve(Count);
byte[] bytes = new byte[16];
for (int i = 0; i < Count; i++)
{
random.NextBytes(bytes);
_values[i] = new Guid(bytes);
_bytes[i] = GuidArray.GuidToBytes(_values[i]);
}
SetGuid();
_array = _setBuilder.Build();
for (int i = 0; i < Count; i++)
if (_array.GetGuid(i) != _values[i]) throw new
Exception("GUID round trip failed");
}
[Benchmark]
public GuidArray.Builder SetGuid()
{
var builder = _setBuilder;
for (int i = 0; i < Count; i++) builder.Set(i, _values[i]);
return builder;
}
[Benchmark]
public GuidArray.Builder SetBytes()
{
var builder = _setBuilder;
for (int i = 0; i < Count; i++) builder.Set(i, _bytes[i]);
return builder;
}
[GlobalCleanup]
public void Cleanup() => _array.Dispose();
}
}
```
</details>
Also ran some benchmarks for different values of ByteWidth:
### Before
| Method | Job | Runtime | Count | Mean | Error | StdDev | Ratio |
RatioSD | Allocated | Alloc Ratio |
|------------ |---------- |---------- |------
|----------:|----------:|----------:|------:|--------:|----------:|------------:|
| SetBytes_4 | .NET 10.0 | .NET 10.0 | 10000 | 103.39 μs | 0.680 μs |
1.018 μs | 1.12 | 0.01 | - | NA |
| SetBytes_4 | .NET 8.0 | .NET 8.0 | 10000 | 92.33 μs | 0.581 μs | 0.814
μs | 1.00 | 0.01 | - | NA |
| | | | | | | | | | | |
| SetBytes_8 | .NET 10.0 | .NET 10.0 | 10000 | 205.57 μs | 24.169 μs |
34.662 μs | 1.38 | 0.23 | - | NA |
| SetBytes_8 | .NET 8.0 | .NET 8.0 | 10000 | 148.93 μs | 2.049 μs |
3.004 μs | 1.00 | 0.03 | - | NA |
| | | | | | | | | | | |
| SetBytes_16 | .NET 10.0 | .NET 10.0 | 10000 | 307.22 μs | 1.168 μs |
1.638 μs | 1.16 | 0.02 | - | NA |
| SetBytes_16 | .NET 8.0 | .NET 8.0 | 10000 | 265.20 μs | 3.166 μs |
4.738 μs | 1.00 | 0.02 | - | NA |
| | | | | | | | | | | |
| SetBytes_32 | .NET 10.0 | .NET 10.0 | 10000 | 579.55 μs | 2.377 μs |
3.254 μs | 1.16 | 0.02 | - | NA |
| SetBytes_32 | .NET 8.0 | .NET 8.0 | 10000 | 497.96 μs | 5.542 μs |
7.948 μs | 1.00 | 0.02 | - | NA |
### After
| Method | Job | Runtime | Count | Mean | Error | StdDev | Median |
Ratio | RatioSD | Allocated | Alloc Ratio |
|------------ |---------- |---------- |------
|---------:|---------:|---------:|---------:|------:|--------:|----------:|------------:|
| SetBytes_4 | .NET 10.0 | .NET 10.0 | 10000 | 62.43 μs | 0.610 μs |
0.914 μs | 62.26 μs | 0.74 | 0.01 | - | NA |
| SetBytes_4 | .NET 8.0 | .NET 8.0 | 10000 | 84.29 μs | 0.356 μs | 0.522
μs | 84.20 μs | 1.00 | 0.01 | - | NA |
| | | | | | | | | | | | |
| SetBytes_8 | .NET 10.0 | .NET 10.0 | 10000 | 69.49 μs | 4.899 μs |
7.181 μs | 75.72 μs | 0.82 | 0.08 | - | NA |
| SetBytes_8 | .NET 8.0 | .NET 8.0 | 10000 | 85.25 μs | 1.264 μs | 1.892
μs | 84.68 μs | 1.00 | 0.03 | - | NA |
| | | | | | | | | | | | |
| SetBytes_16 | .NET 10.0 | .NET 10.0 | 10000 | 61.86 μs | 0.297 μs |
0.407 μs | 61.83 μs | 0.73 | 0.01 | - | NA |
| SetBytes_16 | .NET 8.0 | .NET 8.0 | 10000 | 84.78 μs | 0.803 μs |
1.201 μs | 84.42 μs | 1.00 | 0.02 | - | NA |
| | | | | | | | | | | | |
| SetBytes_32 | .NET 10.0 | .NET 10.0 | 10000 | 62.44 μs | 0.506 μs |
0.709 μs | 62.09 μs | 0.73 | 0.02 | - | NA |
| SetBytes_32 | .NET 8.0 | .NET 8.0 | 10000 | 86.12 μs | 1.471 μs |
2.061 μs | 84.99 μs | 1.00 | 0.03 | - | NA |
---
src/Apache.Arrow/Arrays/FixedSizeBinaryArray.cs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/Apache.Arrow/Arrays/FixedSizeBinaryArray.cs
b/src/Apache.Arrow/Arrays/FixedSizeBinaryArray.cs
index 2bc1965..c67a277 100644
--- a/src/Apache.Arrow/Arrays/FixedSizeBinaryArray.cs
+++ b/src/Apache.Arrow/Arrays/FixedSizeBinaryArray.cs
@@ -222,10 +222,7 @@ namespace Apache.Arrow.Arrays
public TBuilder Set(int index, ReadOnlySpan<byte> value)
{
int startIndex = index * ByteWidth;
- for (int i = 0; i < ByteWidth; i++)
- {
- ValueBuffer.Span[startIndex + i] = value[i];
- }
+ value.Slice(0,
ByteWidth).CopyTo(ValueBuffer.Span.Slice(startIndex, ByteWidth));
ValidityBuffer.Set(index, true);
return Instance;