Span<T>
offers an extremely competitive alternative without having to throw confusing and/or non-portable fluff into your own application's code base:
// byte[] is implicitly convertible to ReadOnlySpan<byte>
static bool ByteArraysEqual(ReadOnlySpan<byte> a1, ReadOnlySpan<byte> a2)
{
return a1.SequenceEqual(a2);
}
The (guts of the) implementation as of .NET 8.0.0 can be found here... sometimes. In the releases since I first posted this answer, the team has added a conditional path that will sometimes use an optimized CLR intrinsic whose guts can be found here instead. You can see the initial motivation and implementation in dotnet/runtime#83945. It's pretty compelling stuff, though at least in the .NET 8.0.0 version, it won't help most people coming to this page, since the intrinsic part only kicks in when the JIT compiler can "see" the length as a constant.
I've revised @EliArbel's gist to add this method as SpansEqual
, drop most of the less interesting performers in others' benchmarks, run it with different array sizes, output graphs, and mark SpansEqual
as the baseline so that it reports how the different methods compare to SpansEqual
. I've also switched to using Linux for these comparisons in the future, which should be mostly transparent except for PInvokeMemcmp
, which now uses my system's libc
instead of msvcrt
.
The below numbers are from the results, lightly edited to remove "Error" column.
| Method | ByteCount | Mean | StdDev | Ratio | RatioSD |
|-------------- |----------- |------------------:|----------------:|------:|--------:|
| SpansEqual | 15 | 2.039 ns | 0.0006 ns | 1.00 | 0.00 |
| LongPointers | 15 | 2.254 ns | 0.0040 ns | 1.11 | 0.00 |
| Unrolled | 15 | 8.276 ns | 0.0048 ns | 4.06 | 0.00 |
| PInvokeMemcmp | 15 | 5.065 ns | 0.0077 ns | 2.48 | 0.00 |
| | | | | | |
| SpansEqual | 1026 | 14.363 ns | 0.0861 ns | 1.00 | 0.00 |
| LongPointers | 1026 | 36.848 ns | 0.0675 ns | 2.57 | 0.02 |
| Unrolled | 1026 | 20.619 ns | 0.0094 ns | 1.44 | 0.01 |
| PInvokeMemcmp | 1026 | 11.995 ns | 0.0347 ns | 0.84 | 0.00 |
| | | | | | |
| SpansEqual | 1048585 | 16,410.211 ns | 30.1120 ns | 1.00 | 0.00 |
| LongPointers | 1048585 | 38,607.983 ns | 165.0060 ns | 2.35 | 0.01 |
| Unrolled | 1048585 | 28,857.431 ns | 19.9256 ns | 1.76 | 0.00 |
| PInvokeMemcmp | 1048585 | 14,869.920 ns | 119.6501 ns | 0.91 | 0.01 |
| | | | | | |
| SpansEqual | 2147483591 | 38,986,402.263 ns | 133,647.1174 ns | 1.00 | 0.00 |
| LongPointers | 2147483591 | 76,738,513.333 ns | 18,573.6696 ns | 1.97 | 0.01 |
| Unrolled | 2147483591 | 56,399,801.524 ns | 188,024.0981 ns | 1.45 | 0.01 |
| PInvokeMemcmp | 2147483591 | 39,772,546.319 ns | 10,138.7623 ns | 1.02 | 0.00 |
I was surprised to see SpansEqual
not come out on top for the max-array-size methods, but the difference is so minor that I don't think it'll ever matter. After refreshing to run on .NET 6.0.4 with my newer hardware, SpansEqual
now comfortably outperforms all others at all array sizes. And after refreshing to run on .NET 8.0.0 (and, more pertinently here, to use my Linux system's libc
instead of my old Windows system's msvcrt
), PInvokeMemcmp
takes a slight edge at certain array sizes, though not the largest or the smallest size.
My system info:
BenchmarkDotNet v0.13.11, EndeavourOS
AMD Ryzen 9 7950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100
[Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI