BinaryDecoder: zero-length arrays share Array.Empty

ReadArrayElements and ReadFixedWidthArray allocated a new T[0] for a
zero-length array, which every response header has (StringTable,
DiagnosticInfos). A length of -1 still decodes as a null array.

ServerLoadHarness read --nodes 1 (20 sessions, 4 alternating rounds):
client allocB/op 1871 -> 1822 on top of the channel timer change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roman Ettlinger
2026-10-19 13:36:49 +02:00
co-authored by Claude Opus 5.5
parent 77e2ca0b5a
commit 730cec1985
2 changed files with 39 additions and 0 deletions
@@ -2432,6 +2432,12 @@ namespace Opc.Ua
private T[] ReadArrayElements<T, TReader>(int length, TReader reader)
where TReader : struct, IElementReader<T>
{
// an empty array is immutable: all share one instance.
if (length == 0)
{
return Array.Empty<T>();
}
int capacity = GetInitialArrayCapacity<T>(length);
if (capacity < length)
{
@@ -2526,6 +2532,11 @@ namespace Opc.Ua
/// against the remaining bytes by the caller.</param>
private T[] ReadFixedWidthArray<T>(int length) where T : unmanaged
{
if (length == 0)
{
return Array.Empty<T>();
}
// The caller verified the length against the remaining bytes. If
// they are unknown the array grows with the bytes actually read.
int capacity = GetRemainingLength() >= 0
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Moq;
using NUnit.Framework;
using Opc.Ua.Tests;
@@ -767,6 +768,33 @@ namespace Opc.Ua.Types.Tests.Encoders
Assert.That(result.Count, Is.Zero);
}
/// <summary>
/// Zero-length arrays, such as the empty StringTable of every response
/// header, decode as empty, not null, and allocate nothing.
/// </summary>
[Test]
public void ReadZeroLengthArraysReturnsEmptyWithoutAllocating()
{
ITelemetryContext telemetryContext = NUnitTelemetryContext.Create();
var messageContext = ServiceMessageContext.CreateEmpty(telemetryContext);
byte[] buffer = [0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF];
using var decoder = new BinaryDecoder(buffer, messageContext);
ArrayOf<string?> strings = decoder.ReadStringArray(null);
ArrayOf<int> integers = decoder.ReadInt32Array(null);
ArrayOf<string?> nullStrings = decoder.ReadStringArray(null);
Assert.That(strings.IsNull, Is.False);
Assert.That(strings.Count, Is.Zero);
Assert.That(MemoryMarshal.TryGetArray(strings.Memory, out ArraySegment<string?> stringArray), Is.True);
Assert.That(stringArray.Array, Is.SameAs(Array.Empty<string?>()));
Assert.That(integers.IsNull, Is.False);
Assert.That(integers.Count, Is.Zero);
Assert.That(MemoryMarshal.TryGetArray(integers.Memory, out ArraySegment<int> integerArray), Is.True);
Assert.That(integerArray.Array, Is.SameAs(Array.Empty<int>()));
Assert.That(nullStrings.IsNull, Is.True);
}
[Test]
public void ReadEncodeableArrayReturnsMultipleElements()
{