.NET 11 BitArray span constructors remove a small but repeatable allocation from parsers that already receive data as a slice, a stack-backed buffer, or another ReadOnlySpan<T>. Instead of calling ToArray() only to satisfy a BitArray constructor, .NET 11 accepts spans of bool, byte, and int directly.

The migration is simple, but production code still needs to preserve the original bit ordering, validate frame lengths, and prove that the source slice and the resulting BitArray are independent. This guide applies the new API to a binary flag parser, adds regression tests, and shows how to measure the allocation change without turning an upstream benchmark into a universal performance promise.

Use .NET 11 BitArray span constructors in a byte parser

Assume a network frame contains a two-byte flag field after a four-byte header. On .NET 10, the parser has to materialize the slice before constructing BitArray:

using System.Collections;

static BitArray ParseFlagsNet10(ReadOnlySpan<byte> frame)
{
    const int headerLength = 4;
    const int flagLength = 2;

    if (frame.Length < headerLength + flagLength)
    {
        throw new ArgumentException("Frame is too short.", nameof(frame));
    }

    ReadOnlySpan<byte> flagBytes = frame.Slice(headerLength, flagLength);
    return new BitArray(flagBytes.ToArray());
}

The allocation is not caused by the parser’s data model; it exists only because the older constructor requires byte[]. In a hot path, that short-lived array adds garbage-collector pressure for every parsed frame.

Targeting .NET 11 changes only the final line:

using System.Collections;

static BitArray ParseFlagsNet11(ReadOnlySpan<byte> frame)
{
    const int headerLength = 4;
    const int flagLength = 2;

    if (frame.Length < headerLength + flagLength)
    {
        throw new ArgumentException("Frame is too short.", nameof(frame));
    }

    ReadOnlySpan<byte> flagBytes = frame.Slice(headerLength, flagLength);
    return new BitArray(flagBytes);
}

The constructor reads the span during construction and builds its own storage. The returned BitArray does not retain a reference to the caller’s span, so a stack-backed source is valid and later changes to the source do not rewrite the bit array.

Preserve BitArray byte ordering

Removing ToArray() must not change how bits are indexed. For the byte constructor, bit zero is the least-significant bit of the first byte. The second byte starts at index eight.

ReadOnlySpan<byte> payload =
    stackalloc byte[] { 0b_0000_0101, 0b_1000_0000 };

var bits = new BitArray(payload);

Console.WriteLine(bits.Length); // 16
Console.WriteLine(bits[0]);     // true: first byte, bit 0
Console.WriteLine(bits[2]);     // true: first byte, bit 2
Console.WriteLine(bits[15]);    // true: second byte, bit 7

The example uses deliberately asymmetric values so a reversed interpretation is obvious. Do not replace a protocol’s documented network-bit numbering with BitArray indexing by assumption; map the protocol definition explicitly and keep tests for the first, last, and cross-byte flags.

If a protocol numbers the most-significant bit as flag zero, the constructor is still usable, but the parser needs a documented translation. That mapping belongs at the protocol boundary, not scattered through business code.

Use Boolean and integer spans deliberately

.NET 11 adds three overloads: ReadOnlySpan<bool>, ReadOnlySpan<byte>, and ReadOnlySpan<int>. Choose the overload that matches the representation you already own rather than converting between representations only to reach a constructor.

  • Boolean spans: each element becomes one bit, preserving element order.
  • Byte spans: each byte contributes eight bits using the established BitArray(byte[]) ordering.
  • Integer spans: each 32-bit integer contributes 32 bits using the same semantics as BitArray(int[]).
Span<bool> enabled = stackalloc bool[] { true, false, true };
var featureFlags = new BitArray(enabled);

Span<int> masks = stackalloc int[] { 0x0000_0003, unchecked((int)0x8000_0000) };
var permissionBits = new BitArray(masks);

Console.WriteLine(featureFlags.Length);  // 3
Console.WriteLine(permissionBits.Length); // 64

The Boolean overload is useful when an earlier stage already produced logical values; it is not a reason to expand compact bytes into Booleans first. Likewise, use the integer overload for existing word-oriented masks, not as a substitute for parsing an external byte order correctly.

Prove parity and source independence

A safe migration compares the old and new construction paths for representative inputs. It also mutates the source after construction to prove the result owns independent state.

using System.Collections;
using Xunit;

public sealed class BitArraySpanTests
{
    [Theory]
    [InlineData(new byte[] { 0x00 })]
    [InlineData(new byte[] { 0x01, 0x80 })]
    [InlineData(new byte[] { 0xFF, 0x55, 0xAA })]
    public void Span_constructor_matches_array_constructor(byte[] source)
    {
        var expected = new BitArray(source.ToArray());
        var actual = new BitArray(source.AsSpan());

        Assert.Equal(expected.Length, actual.Length);
        for (int i = 0; i < expected.Length; i++)
        {
            Assert.Equal(expected[i], actual[i]);
        }
    }

    [Fact]
    public void Result_does_not_change_when_source_changes()
    {
        byte[] source = { 0b_0000_0001 };
        var bits = new BitArray(source.AsSpan());

        source[0] = 0;

        Assert.True(bits[0]);
    }
}

The parity test protects every bit, not merely the total length or one happy-path flag. The independence test protects future refactors from assuming that BitArray is a zero-copy view over mutable source memory; the allocation removed is the temporary input array, not the result’s own storage.

Add protocol-specific cases for empty input, minimum and maximum accepted frame lengths, sliced buffers with nonzero offsets, all-zero and all-one values, and flags that cross byte or integer boundaries. Validate length before slicing so malformed input fails with your parser’s contract rather than an incidental range exception.

Measure allocations in your parser

The .NET 11 RC1 release notes report that direct span construction was 33–51% faster than the span.ToArray() workaround in the upstream Windows Arm64 benchmarks. They also report approximately 50% lower allocations for byte and integer inputs and up to 89% lower allocations for Boolean inputs. Those numbers describe that benchmark matrix; they are evidence for testing the .NET 11 BitArray span constructors, not a forecast for every service.

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class FlagParserBenchmarks
{
    private readonly byte[] _frame = { 1, 2, 3, 4, 0x05, 0x80 };

    [Benchmark(Baseline = true)]
    public BitArray WithTemporaryArray() =>
        new(_frame.AsSpan(4, 2).ToArray());

    [Benchmark]
    public BitArray WithSpan() =>
        new(_frame.AsSpan(4, 2));
}

Run the benchmark with the same SDK, architecture, runtime settings, and representative slice sizes used by production. Compare both allocated bytes and throughput, then profile the complete parser: removing a two-byte temporary array may matter at millions of frames per minute and be irrelevant in a rarely used configuration path.

Handle compatibility and rollout

The new overloads require compiling for .NET 11. Pin the exact SDK in global.json and make CI fail when a runner silently selects another feature band; the .NET 11 SDK health check guide shows how to make the selected SDK visible and auditable.

{
  "sdk": {
    "version": "11.0.100-rc.1.26425.128",
    "rollForward": "disable"
  }
}

The exact RC1 build shown here was current on September 22, 2026. Update the pin deliberately when a supported servicing build or general-availability SDK replaces it, and rerun parity plus allocation checks after the change.

For a multi-targeted library, keep the old array path for earlier target frameworks and compile the span constructor only for .NET 11 or later:

static BitArray CreateBits(ReadOnlySpan<byte> bytes)
{
#if NET11_0_OR_GREATER
    return new BitArray(bytes);
#else
    return new BitArray(bytes.ToArray());
#endif
}

This preserves behavior for existing consumers while removing the compatibility allocation where the API exists. Do not hide the target-framework split behind reflection or exception-driven probing; compile-time selection is clearer and easier to test.

Roll out by capturing allocation rate, Gen 0 collection rate, parser latency, malformed-frame counts, and protocol parity. The rollback is a source-level return to ToArray(); because the resulting bits must remain identical, any semantic difference is a correctness failure rather than an acceptable performance trade.

Adoption checklist

  • Target .NET 11 and pin the exact SDK used by local development and CI.
  • Replace only temporary-array conversions that exist to call a BitArray constructor.
  • Keep the protocol’s byte order and bit-numbering contract explicit.
  • Test parity for slices, boundary flags, all-zero/all-one values, and malformed lengths.
  • Prove the resulting BitArray is independent from later source mutations.
  • Measure allocation and throughput changes on the production architecture and realistic input sizes.
  • Retain the array path for earlier target frameworks when multi-targeting.
  • Monitor parser correctness and GC behavior, with a source-level rollback ready.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock