簡體   English   中英

比較 .NET 中的兩個字節數組

[英]Comparing two byte arrays in .NET

我怎樣才能快速做到這一點?

當然我可以這樣做:

static bool ByteArrayCompare(byte[] a1, byte[] a2)
{
    if (a1.Length != a2.Length)
        return false;

    for (int i=0; i<a1.Length; i++)
        if (a1[i]!=a2[i])
            return false;

    return true;
}

但我正在尋找BCL函數或一些高度優化的行之有效的方法來做到這一點。

java.util.Arrays.equals((sbyte[])(Array)a1, (sbyte[])(Array)a2);

效果很好,但它看起來不適用於 x64。

請注意我在這里的超快速回答。

您可以使用Enumerable.SequenceEqual方法。

using System;
using System.Linq;
...
var a1 = new int[] { 1, 2, 3};
var a2 = new int[] { 1, 2, 3};
var a3 = new int[] { 1, 2, 4};
var x = a1.SequenceEqual(a2); // true
var y = a1.SequenceEqual(a3); // false

如果由於某種原因您不能使用 .NET 3.5,那么您的方法是可以的。
編譯器\\運行時環境將優化您的循環,因此您無需擔心性能。

P/Invoke力量激活!

[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
static extern int memcmp(byte[] b1, byte[] b2, long count);

static bool ByteArrayCompare(byte[] b1, byte[] b2)
{
    // Validate buffers are the same length.
    // This also ensures that the count does not exceed the length of either buffer.  
    return b1.Length == b2.Length && memcmp(b1, b2, b1.Length) == 0;
}

.NET 4 中有一個新的內置解決方案 - IStructuralEquatable

static bool ByteArrayCompare(byte[] a1, byte[] a2) 
{
    return StructuralComparisons.StructuralEqualityComparer.Equals(a1, a2);
}

Span<T>提供了一種極具競爭力的替代方案,而不必將令人困惑和/或不可移植的絨毛扔到您自己的應用程序的代碼庫中:

// byte[] is implicitly convertible to ReadOnlySpan<byte>
static bool ByteArrayCompare(ReadOnlySpan<byte> a1, ReadOnlySpan<byte> a2)
{
    return a1.SequenceEqual(a2);
}

.NET 5.0.0 的(核心)實現可以在這里找到。

我已經修改了@EliArbel 的要點,將這個方法添加為SpansEqual ,在其他人的基准測試中刪除大多數不太有趣的執行者,使用不同的數組大小、輸出圖運行它,並將SpansEqual標記為基線,以便它報告不同方法的方式與SpansEqual進行比較。

以下數字來自結果,經過輕微編輯以刪除“錯誤”列。

|        Method |  ByteCount |               Mean |            StdDev | Ratio | RatioSD |
|-------------- |----------- |-------------------:|------------------:|------:|--------:|
|    SpansEqual |         15 |           4.629 ns |         0.0289 ns |  1.00 |    0.00 |
|  LongPointers |         15 |           4.598 ns |         0.0416 ns |  0.99 |    0.01 |
|      Unrolled |         15 |          18.199 ns |         0.0291 ns |  3.93 |    0.02 |
| PInvokeMemcmp |         15 |           9.872 ns |         0.0441 ns |  2.13 |    0.02 |
|               |            |                    |                   |       |         |
|    SpansEqual |       1026 |          19.965 ns |         0.0880 ns |  1.00 |    0.00 |
|  LongPointers |       1026 |          63.005 ns |         0.5217 ns |  3.16 |    0.04 |
|      Unrolled |       1026 |          38.731 ns |         0.0166 ns |  1.94 |    0.01 |
| PInvokeMemcmp |       1026 |          40.355 ns |         0.0202 ns |  2.02 |    0.01 |
|               |            |                    |                   |       |         |
|    SpansEqual |    1048585 |      43,761.339 ns |        30.8744 ns |  1.00 |    0.00 |
|  LongPointers |    1048585 |      59,585.479 ns |        17.3907 ns |  1.36 |    0.00 |
|      Unrolled |    1048585 |      54,646.243 ns |        35.7638 ns |  1.25 |    0.00 |
| PInvokeMemcmp |    1048585 |      55,198.289 ns |        23.9732 ns |  1.26 |    0.00 |
|               |            |                    |                   |       |         |
|    SpansEqual | 2147483591 | 240,607,692.857 ns | 2,733,489.4894 ns |  1.00 |    0.00 |
|  LongPointers | 2147483591 | 238,223,478.571 ns | 2,033,769.5979 ns |  0.99 |    0.02 |
|      Unrolled | 2147483591 | 236,227,340.000 ns | 2,189,627.0164 ns |  0.98 |    0.00 |
| PInvokeMemcmp | 2147483591 | 238,724,660.000 ns | 3,726,140.4720 ns |  0.99 |    0.02 |

我很驚訝地看到SpansEqual沒有在 max-array-size 方法中名列前茅,但差異很小,我認為這無關緊要。

我的系統信息:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100
  [Host]     : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
  DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT

用戶gil建議產生此解決方案的不安全代碼:

// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
static unsafe bool UnsafeCompare(byte[] a1, byte[] a2) {
  if(a1==a2) return true;
  if(a1==null || a2==null || a1.Length!=a2.Length)
    return false;
  fixed (byte* p1=a1, p2=a2) {
    byte* x1=p1, x2=p2;
    int l = a1.Length;
    for (int i=0; i < l/8; i++, x1+=8, x2+=8)
      if (*((long*)x1) != *((long*)x2)) return false;
    if ((l & 4)!=0) { if (*((int*)x1)!=*((int*)x2)) return false; x1+=4; x2+=4; }
    if ((l & 2)!=0) { if (*((short*)x1)!=*((short*)x2)) return false; x1+=2; x2+=2; }
    if ((l & 1)!=0) if (*((byte*)x1) != *((byte*)x2)) return false;
    return true;
  }
}

它對盡可能多的數組進行基於 64 位的比較。 這種依賴於數組開始 qword 對齊的事實。 如果不是 qword 對齊,它會起作用,只是沒有它那么快。

它比簡單的for循環快 7 個定時器。 使用 J# 庫的性能與原始for循環等效。 使用 .SequenceEqual 運行速度大約慢七倍; 我認為只是因為它使用的是 IEnumerator.MoveNext。 我想基於 LINQ 的解決方案至少會那么慢或更糟。

如果您不反對這樣做,您可以導入 J# 程序集“vjslib.dll”並使用其Arrays.equals(byte[], byte[]) 方法...

不過,如果有人嘲笑你,請不要怪我......


編輯:對於它的價值,我使用 Reflector 來反匯編代碼,這是它的樣子:

public static bool equals(sbyte[] a1, sbyte[] a2)
{
  if (a1 == a2)
  {
    return true;
  }
  if ((a1 != null) && (a2 != null))
  {
    if (a1.Length != a2.Length)
    {
      return false;
    }
    for (int i = 0; i < a1.Length; i++)
    {
      if (a1[i] != a2[i])
      {
        return false;
      }
    }
    return true;
  }
  return false;
}

.NET 3.5 和更新版本有一個新的公共類型System.Data.Linq.Binary封裝了byte[] 它實現了IEquatable<Binary> ,它(實際上)比較了兩個字節數組。 請注意, System.Data.Linq.Binary也有來自byte[]隱式轉換運算符。

MSDN 文檔: System.Data.Linq.Binary

Equals 方法的反射器反編譯:

private bool EqualsTo(Binary binary)
{
    if (this != binary)
    {
        if (binary == null)
        {
            return false;
        }
        if (this.bytes.Length != binary.bytes.Length)
        {
            return false;
        }
        if (this.hashCode != binary.hashCode)
        {
            return false;
        }
        int index = 0;
        int length = this.bytes.Length;
        while (index < length)
        {
            if (this.bytes[index] != binary.bytes[index])
            {
                return false;
            }
            index++;
        }
    }
    return true;
}

有趣的是,如果兩個 Binary 對象的哈希值相同,它們只會進行逐字節比較循環。 然而,這是以在Binary對象的構造函數中計算散列為代價的(通過使用for循環遍歷數組 :-) )。

上面的實現意味着在最壞的情況下,您可能必須遍歷數組 3 次:首先計算 array1 的散列,然后計算 array2 的散列,最后(因為這是最壞的情況,長度和散列相等)進行比較數組 1 中的字節與數組 2 中的字節。

總的來說,即使System.Data.Linq.Binary內置在 BCL 中,我也不認為這是比較兩個字節數組的最快方法:-|。

我發布了一個關於檢查 byte[] 是否充滿零的類似問題。 (SIMD 代碼被打敗了,所以我從這個答案中刪除了它。)這是我比較中最快的代碼:

static unsafe bool EqualBytesLongUnrolled (byte[] data1, byte[] data2)
{
    if (data1 == data2)
        return true;
    if (data1.Length != data2.Length)
        return false;

    fixed (byte* bytes1 = data1, bytes2 = data2) {
        int len = data1.Length;
        int rem = len % (sizeof(long) * 16);
        long* b1 = (long*)bytes1;
        long* b2 = (long*)bytes2;
        long* e1 = (long*)(bytes1 + len - rem);

        while (b1 < e1) {
            if (*(b1) != *(b2) || *(b1 + 1) != *(b2 + 1) || 
                *(b1 + 2) != *(b2 + 2) || *(b1 + 3) != *(b2 + 3) ||
                *(b1 + 4) != *(b2 + 4) || *(b1 + 5) != *(b2 + 5) || 
                *(b1 + 6) != *(b2 + 6) || *(b1 + 7) != *(b2 + 7) ||
                *(b1 + 8) != *(b2 + 8) || *(b1 + 9) != *(b2 + 9) || 
                *(b1 + 10) != *(b2 + 10) || *(b1 + 11) != *(b2 + 11) ||
                *(b1 + 12) != *(b2 + 12) || *(b1 + 13) != *(b2 + 13) || 
                *(b1 + 14) != *(b2 + 14) || *(b1 + 15) != *(b2 + 15))
                return false;
            b1 += 16;
            b2 += 16;
        }

        for (int i = 0; i < rem; i++)
            if (data1 [len - 1 - i] != data2 [len - 1 - i])
                return false;

        return true;
    }
}

在兩個 256MB 字節數組上測量:

UnsafeCompare                           : 86,8784 ms
EqualBytesSimd                          : 71,5125 ms
EqualBytesSimdUnrolled                  : 73,1917 ms
EqualBytesLongUnrolled                  : 39,8623 ms

讓我們再添加一個!

最近微軟發布了一個特殊的 NuGet 包System.Runtime.CompilerServices.Unsafe 它很特別,因為它是用IL編寫的,並提供 C# 中不直接可用的低級功能。

其中一個方法Unsafe.As<T>(object)允許將任何引用類型轉換為另一種引用類型,跳過任何安全檢查。 這通常是一個非常糟糕的主意,但如果兩種類型具有相同的結構,它就可以工作。 所以我們可以使用它來將byte[]轉換為long[]

bool CompareWithUnsafeLibrary(byte[] a1, byte[] a2)
{
    if (a1.Length != a2.Length) return false;

    var longSize = (int)Math.Floor(a1.Length / 8.0);
    var long1 = Unsafe.As<long[]>(a1);
    var long2 = Unsafe.As<long[]>(a2);

    for (var i = 0; i < longSize; i++)
    {
        if (long1[i] != long2[i]) return false;
    }

    for (var i = longSize * 8; i < a1.Length; i++)
    {
        if (a1[i] != a2[i]) return false;
    }

    return true;
}

請注意, long1.Length仍會返回原始數組的長度,因為它存儲在數組內存結構的一個字段中。

此方法不如此處演示的其他方法快,但它比原始方法快得多,不使用不安全代碼或 P/Invoke 或固定,並且實現非常簡單 (IMO)。 以下是我的機器的一些BenchmarkDotNet結果:

BenchmarkDotNet=v0.10.3.0, OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4870HQ CPU 2.50GHz, ProcessorCount=8
Frequency=2435775 Hz, Resolution=410.5470 ns, Timer=TSC
  [Host]     : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
  DefaultJob : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0

                 Method |          Mean |    StdDev |
----------------------- |-------------- |---------- |
          UnsafeLibrary |   125.8229 ns | 0.3588 ns |
          UnsafeCompare |    89.9036 ns | 0.8243 ns |
           JSharpEquals | 1,432.1717 ns | 1.3161 ns |
 EqualBytesLongUnrolled |    43.7863 ns | 0.8923 ns |
              NewMemCmp |    65.4108 ns | 0.2202 ns |
            ArraysEqual |   910.8372 ns | 2.6082 ns |
          PInvokeMemcmp |    52.7201 ns | 0.1105 ns |

我還創建了一個 包含所有測試要點

 using System.Linq; //SequenceEqual

 byte[] ByteArray1 = null;
 byte[] ByteArray2 = null;

 ByteArray1 = MyFunct1();
 ByteArray2 = MyFunct2();

 if (ByteArray1.SequenceEqual<byte>(ByteArray2) == true)
 {
    MessageBox.Show("Match");
 }
 else
 {
   MessageBox.Show("Don't match");
 }

我開發了一種方法,它在我的 PC 上略勝於memcmp() (底座的答案)並且非常EqualBytesLongUnrolled() (Arek Bulski 的答案)。 基本上,它將循環展開 4 而不是 8。

2019 年 3 月 30 日更新

從 .NET core 3.0 開始,我們有 SIMD 支持!

此解決方案在我的 PC 上的速度要快得多:

#if NETCOREAPP3_0
using System.Runtime.Intrinsics.X86;
#endif
…

public static unsafe bool Compare(byte[] arr0, byte[] arr1)
{
    if (arr0 == arr1)
    {
        return true;
    }
    if (arr0 == null || arr1 == null)
    {
        return false;
    }
    if (arr0.Length != arr1.Length)
    {
        return false;
    }
    if (arr0.Length == 0)
    {
        return true;
    }
    fixed (byte* b0 = arr0, b1 = arr1)
    {
#if NETCOREAPP3_0
        if (Avx2.IsSupported)
        {
            return Compare256(b0, b1, arr0.Length);
        }
        else if (Sse2.IsSupported)
        {
            return Compare128(b0, b1, arr0.Length);
        }
        else
#endif
        {
            return Compare64(b0, b1, arr0.Length);
        }
    }
}
#if NETCOREAPP3_0
public static unsafe bool Compare256(byte* b0, byte* b1, int length)
{
    byte* lastAddr = b0 + length;
    byte* lastAddrMinus128 = lastAddr - 128;
    const int mask = -1;
    while (b0 < lastAddrMinus128) // unroll the loop so that we are comparing 128 bytes at a time.
    {
        if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(b0), Avx.LoadVector256(b1))) != mask)
        {
            return false;
        }
        if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(b0 + 32), Avx.LoadVector256(b1 + 32))) != mask)
        {
            return false;
        }
        if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(b0 + 64), Avx.LoadVector256(b1 + 64))) != mask)
        {
            return false;
        }
        if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(b0 + 96), Avx.LoadVector256(b1 + 96))) != mask)
        {
            return false;
        }
        b0 += 128;
        b1 += 128;
    }
    while (b0 < lastAddr)
    {
        if (*b0 != *b1) return false;
        b0++;
        b1++;
    }
    return true;
}
public static unsafe bool Compare128(byte* b0, byte* b1, int length)
{
    byte* lastAddr = b0 + length;
    byte* lastAddrMinus64 = lastAddr - 64;
    const int mask = 0xFFFF;
    while (b0 < lastAddrMinus64) // unroll the loop so that we are comparing 64 bytes at a time.
    {
        if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(b0), Sse2.LoadVector128(b1))) != mask)
        {
            return false;
        }
        if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(b0 + 16), Sse2.LoadVector128(b1 + 16))) != mask)
        {
            return false;
        }
        if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(b0 + 32), Sse2.LoadVector128(b1 + 32))) != mask)
        {
            return false;
        }
        if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(b0 + 48), Sse2.LoadVector128(b1 + 48))) != mask)
        {
            return false;
        }
        b0 += 64;
        b1 += 64;
    }
    while (b0 < lastAddr)
    {
        if (*b0 != *b1) return false;
        b0++;
        b1++;
    }
    return true;
}
#endif
public static unsafe bool Compare64(byte* b0, byte* b1, int length)
{
    byte* lastAddr = b0 + length;
    byte* lastAddrMinus32 = lastAddr - 32;
    while (b0 < lastAddrMinus32) // unroll the loop so that we are comparing 32 bytes at a time.
    {
        if (*(ulong*)b0 != *(ulong*)b1) return false;
        if (*(ulong*)(b0 + 8) != *(ulong*)(b1 + 8)) return false;
        if (*(ulong*)(b0 + 16) != *(ulong*)(b1 + 16)) return false;
        if (*(ulong*)(b0 + 24) != *(ulong*)(b1 + 24)) return false;
        b0 += 32;
        b1 += 32;
    }
    while (b0 < lastAddr)
    {
        if (*b0 != *b1) return false;
        b0++;
        b1++;
    }
    return true;
}

如果您查看 .NET 如何執行 string.Equals,您會發現它使用名為 EqualsHelper 的私有方法,該方法具有“不安全”的指針實現。 .NET Reflector是您了解內部工作方式的好幫手。

這可以用作字節數組比較的模板,我在博客文章Fast byte array compare in C# 中做了一個實現。 我還做了一些基本的基准測試,看看什么時候安全的實現比不安全的更快。

也就是說,除非您真的需要出色的性能,否則我會進行簡單的 fr 循環比較。

我會使用不安全的代碼並運行for循環比較 Int32 指針。

也許您還應該考慮檢查數組是否為非空。

我使用附帶的程序 .net 4.7 release build 做了一些測量,沒有附加調試器。 我認為人們一直在使用錯誤的度量標准,因為如果您關心速度,那么您需要多長時間才能確定兩個字節數組是否相等。 即以字節為單位的吞吐量。

StructuralComparison :              4.6 MiB/s
for                  :            274.5 MiB/s
ToUInt32             :            263.6 MiB/s
ToUInt64             :            474.9 MiB/s
memcmp               :           8500.8 MiB/s

如您所見,沒有比memcmp更好的方法了,而且它的速度要快memcmp數量級。 一個簡單的for循環是第二好的選擇。 我仍然不明白為什么 Microsoft 不能簡單地包含一個Buffer.Compare方法。

[程序.cs]:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace memcmp
{
    class Program
    {
        static byte[] TestVector(int size)
        {
            var data = new byte[size];
            using (var rng = new System.Security.Cryptography.RNGCryptoServiceProvider())
            {
                rng.GetBytes(data);
            }
            return data;
        }

        static TimeSpan Measure(string testCase, TimeSpan offset, Action action, bool ignore = false)
        {
            var t = Stopwatch.StartNew();
            var n = 0L;
            while (t.Elapsed < TimeSpan.FromSeconds(10))
            {
                action();
                n++;
            }
            var elapsed = t.Elapsed - offset;
            if (!ignore)
            {
                Console.WriteLine($"{testCase,-16} : {n / elapsed.TotalSeconds,16:0.0} MiB/s");
            }
            return elapsed;
        }

        [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern int memcmp(byte[] b1, byte[] b2, long count);

        static void Main(string[] args)
        {
            // how quickly can we establish if two sequences of bytes are equal?

            // note that we are testing the speed of different comparsion methods

            var a = TestVector(1024 * 1024); // 1 MiB
            var b = (byte[])a.Clone();

            // was meant to offset the overhead of everything but copying but my attempt was a horrible mistake... should have reacted sooner due to the initially ridiculous throughput values...
            // Measure("offset", new TimeSpan(), () => { return; }, ignore: true);
            var offset = TimeZone.Zero

            Measure("StructuralComparison", offset, () =>
            {
                StructuralComparisons.StructuralEqualityComparer.Equals(a, b);
            });

            Measure("for", offset, () =>
            {
                for (int i = 0; i < a.Length; i++)
                {
                    if (a[i] != b[i]) break;
                }
            });

            Measure("ToUInt32", offset, () =>
            {
                for (int i = 0; i < a.Length; i += 4)
                {
                    if (BitConverter.ToUInt32(a, i) != BitConverter.ToUInt32(b, i)) break;
                }
            });

            Measure("ToUInt64", offset, () =>
            {
                for (int i = 0; i < a.Length; i += 8)
                {
                    if (BitConverter.ToUInt64(a, i) != BitConverter.ToUInt64(b, i)) break;
                }
            });

            Measure("memcmp", offset, () =>
            {
                memcmp(a, b, a.Length);
            });
        }
    }
}

找不到我完全滿意的解決方案(合理的性能,但沒有不安全的代碼/pinvoke)所以我想出了這個,沒有什么真正的原創,但有效:

    /// <summary>
    /// 
    /// </summary>
    /// <param name="array1"></param>
    /// <param name="array2"></param>
    /// <param name="bytesToCompare"> 0 means compare entire arrays</param>
    /// <returns></returns>
    public static bool ArraysEqual(byte[] array1, byte[] array2, int bytesToCompare = 0)
    {
        if (array1.Length != array2.Length) return false;

        var length = (bytesToCompare == 0) ? array1.Length : bytesToCompare;
        var tailIdx = length - length % sizeof(Int64);

        //check in 8 byte chunks
        for (var i = 0; i < tailIdx; i += sizeof(Int64))
        {
            if (BitConverter.ToInt64(array1, i) != BitConverter.ToInt64(array2, i)) return false;
        }

        //check the remainder of the array, always shorter than 8 bytes
        for (var i = tailIdx; i < length; i++)
        {
            if (array1[i] != array2[i]) return false;
        }

        return true;
    }

與此頁面上的其他一些解決方案相比的性能:

簡單循環:19837 個滴答聲,1.00

*BitConverter:4886 滴答,4.06

UnsafeCompare:1636 個滴答聲,12.12

EqualBytesLongUnrolled:637 滴答,31.09

P/Invoke memcmp:369 滴答聲,53.67

在 linqpad 中測試,1000000 字節相同數組(最壞情況),每個數組 500 次迭代。

對於那些關心順序的人(即希望您的memcmp返回一個它應該的int而不是什么),.NET Core 3.0(可能還有 .NET Standard 2.1 aka .NET 5.0)將包含一個Span.SequenceCompareTo(...)擴展方法(加上Span.SequenceEqualTo ),可用於比較兩個ReadOnlySpan<T>實例( where T: IComparable<T> )。

最初的 GitHub 提案中,討論包括與跳轉表計算的方法比較、讀取byte[]作為long[] 、SIMD 使用以及 p/invoke 到 CLR 實現的memcmp

展望未來,這應該是比較字節數組或字節范圍的首選方法(對於 .NET Standard 2.1 API 應該使用Span<byte>而不是byte[] ),並且它足夠快,您不應該不再關心優化它(不,盡管名稱相似,但它的性能不如可怕的Enumerable.SequenceEqual )。

#if NETCOREAPP3_0
// Using the platform-native Span<T>.SequenceEqual<T>(..)
public static int Compare(byte[] range1, int offset1, byte[] range2, int offset2, int count)
{
    var span1 = range1.AsSpan(offset1, count);
    var span2 = range2.AsSpan(offset2, count);

    return span1.SequenceCompareTo(span2);
    // or, if you don't care about ordering
    // return span1.SequenceEqual(span2);
}
#else
// The most basic implementation, in platform-agnostic, safe C#
public static bool Compare(byte[] range1, int offset1, byte[] range2, int offset2, int count)
{
    // Working backwards lets the compiler optimize away bound checking after the first loop
    for (int i = count - 1; i >= 0; --i)
    {
        if (range1[offset1 + i] != range2[offset2 + i])
        {
            return false;
        }
    }

    return true;
}
#endif

從上面的建議來看,似乎EqualBytesLongUnrolled是最好的。

跳過的方法(Enumerable.SequenceEqual、StructuralComparisons.StructuralEqualityComparer.Equals)不是耐心的慢。 在 265MB 陣列上,我測量了這一點:

Host Process Environment Information:
BenchmarkDotNet.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-3770 CPU 3.40GHz, ProcessorCount=8
Frequency=3323582 ticks, Resolution=300.8802 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1590.0

Type=CompareMemoriesBenchmarks  Mode=Throughput  

                 Method |      Median |    StdDev | Scaled | Scaled-SD |
----------------------- |------------ |---------- |------- |---------- |
             NewMemCopy |  30.0443 ms | 1.1880 ms |   1.00 |      0.00 |
 EqualBytesLongUnrolled |  29.9917 ms | 0.7480 ms |   0.99 |      0.04 |
          msvcrt_memcmp |  30.0930 ms | 0.2964 ms |   1.00 |      0.03 |
          UnsafeCompare |  31.0520 ms | 0.7072 ms |   1.03 |      0.04 |
       ByteArrayCompare | 212.9980 ms | 2.0776 ms |   7.06 |      0.25 |

OS=Windows
Processor=?, ProcessorCount=8
Frequency=3323582 ticks, Resolution=300.8802 ns, Timer=TSC
CLR=CORE, Arch=64-bit ? [RyuJIT]
GC=Concurrent Workstation
dotnet cli version: 1.0.0-preview2-003131

Type=CompareMemoriesBenchmarks  Mode=Throughput  

                 Method |      Median |    StdDev | Scaled | Scaled-SD |
----------------------- |------------ |---------- |------- |---------- |
             NewMemCopy |  30.1789 ms | 0.0437 ms |   1.00 |      0.00 |
 EqualBytesLongUnrolled |  30.1985 ms | 0.1782 ms |   1.00 |      0.01 |
          msvcrt_memcmp |  30.1084 ms | 0.0660 ms |   1.00 |      0.00 |
          UnsafeCompare |  31.1845 ms | 0.4051 ms |   1.03 |      0.01 |
       ByteArrayCompare | 212.0213 ms | 0.1694 ms |   7.03 |      0.01 |

為了比較短字節數組,以下是一個有趣的技巧:

if(myByteArray1.Length != myByteArray2.Length) return false;
if(myByteArray1.Length == 8)
   return BitConverter.ToInt64(myByteArray1, 0) == BitConverter.ToInt64(myByteArray2, 0); 
else if(myByteArray.Length == 4)
   return BitConverter.ToInt32(myByteArray2, 0) == BitConverter.ToInt32(myByteArray2, 0); 

然后我可能會遇到問題中列出的解決方案。

對這段代碼進行性能分析會很有趣。

我在這里沒有看到很多 linq 解決方案。

我不確定對性能的影響,但是我通常堅持使用linq作為經驗法則,然后在必要時進行優化。

public bool CompareTwoArrays(byte[] array1, byte[] array2)
 {
   return !array1.Where((t, i) => t != array2[i]).Any();
 }

請注意,這僅在它們是相同大小的數組時才有效。 擴展看起來像這樣

public bool CompareTwoArrays(byte[] array1, byte[] array2)
 {
   if (array1.Length != array2.Length) return false;
   return !array1.Where((t, i) => t != array2[i]).Any();
 }

我想到了許多顯卡內置的塊傳輸加速方法。 但是,您將不得不逐字節復制所有數據,因此,如果您不想在非托管和依賴於硬件的代碼中實現整個邏輯部分,這對您沒有多大幫助......

與上述方法類似的另一種優化方法是從一開始就將盡可能多的數據存儲在 long[] 而不是 byte[] 中,例如,如果您從二進制文件中順序讀取它,或者,如果您使用內存映射文件,則將數據作為 long[] 或單個 long 值讀入。 然后,您的比較循環只需要對包含相同數據量的 byte[] 執行的迭代次數的 1/8。 這是您需要比較的時間和頻率的問題,以及您需要以逐字節的方式訪問數據的時間和頻率的問題,例如,在 API 調用中將其用作期望的方法中的參數一個字節[]。 最后,你只能判斷你是否真的了解用例......

我選擇了一個受 ArekBulski 發布的 EqualBytesLongUnrolled 方法啟發的解決方案,並進行了額外的優化。 在我的例子中,數組中的數組差異往往靠近數組的尾部。 在測試中,我發現對於大型數組,如果能夠以相反的順序比較數組元素,則該解決方案比基於 memcmp 的解決方案具有巨大的性能提升。 這是解決方案:

public enum CompareDirection { Forward, Backward }

private static unsafe bool UnsafeEquals(byte[] a, byte[] b, CompareDirection direction = CompareDirection.Forward)
{
    // returns when a and b are same array or both null
    if (a == b) return true;

    // if either is null or different lengths, can't be equal
    if (a == null || b == null || a.Length != b.Length)
        return false;

    const int UNROLLED = 16;                // count of longs 'unrolled' in optimization
    int size = sizeof(long) * UNROLLED;     // 128 bytes (min size for 'unrolled' optimization)
    int len = a.Length;
    int n = len / size;         // count of full 128 byte segments
    int r = len % size;         // count of remaining 'unoptimized' bytes

    // pin the arrays and access them via pointers
    fixed (byte* pb_a = a, pb_b = b)
    {
        if (r > 0 && direction == CompareDirection.Backward)
        {
            byte* pa = pb_a + len - 1;
            byte* pb = pb_b + len - 1;
            byte* phead = pb_a + len - r;
            while(pa >= phead)
            {
                if (*pa != *pb) return false;
                pa--;
                pb--;
            }
        }

        if (n > 0)
        {
            int nOffset = n * size;
            if (direction == CompareDirection.Forward)
            {
                long* pa = (long*)pb_a;
                long* pb = (long*)pb_b;
                long* ptail = (long*)(pb_a + nOffset);
                while (pa < ptail)
                {
                    if (*(pa + 0) != *(pb + 0) || *(pa + 1) != *(pb + 1) ||
                        *(pa + 2) != *(pb + 2) || *(pa + 3) != *(pb + 3) ||
                        *(pa + 4) != *(pb + 4) || *(pa + 5) != *(pb + 5) ||
                        *(pa + 6) != *(pb + 6) || *(pa + 7) != *(pb + 7) ||
                        *(pa + 8) != *(pb + 8) || *(pa + 9) != *(pb + 9) ||
                        *(pa + 10) != *(pb + 10) || *(pa + 11) != *(pb + 11) ||
                        *(pa + 12) != *(pb + 12) || *(pa + 13) != *(pb + 13) ||
                        *(pa + 14) != *(pb + 14) || *(pa + 15) != *(pb + 15)
                    )
                    {
                        return false;
                    }
                    pa += UNROLLED;
                    pb += UNROLLED;
                }
            }
            else
            {
                long* pa = (long*)(pb_a + nOffset);
                long* pb = (long*)(pb_b + nOffset);
                long* phead = (long*)pb_a;
                while (phead < pa)
                {
                    if (*(pa - 1) != *(pb - 1) || *(pa - 2) != *(pb - 2) ||
                        *(pa - 3) != *(pb - 3) || *(pa - 4) != *(pb - 4) ||
                        *(pa - 5) != *(pb - 5) || *(pa - 6) != *(pb - 6) ||
                        *(pa - 7) != *(pb - 7) || *(pa - 8) != *(pb - 8) ||
                        *(pa - 9) != *(pb - 9) || *(pa - 10) != *(pb - 10) ||
                        *(pa - 11) != *(pb - 11) || *(pa - 12) != *(pb - 12) ||
                        *(pa - 13) != *(pb - 13) || *(pa - 14) != *(pb - 14) ||
                        *(pa - 15) != *(pb - 15) || *(pa - 16) != *(pb - 16)
                    )
                    {
                        return false;
                    }
                    pa -= UNROLLED;
                    pb -= UNROLLED;
                }
            }
        }

        if (r > 0 && direction == CompareDirection.Forward)
        {
            byte* pa = pb_a + len - r;
            byte* pb = pb_b + len - r;
            byte* ptail = pb_a + len;
            while(pa < ptail)
            {
                if (*pa != *pb) return false;
                pa++;
                pb++;
            }
        }
    }

    return true;
}

抱歉,如果您正在尋找一種托管方式,那么您已經在正確地執行此操作,並且據我所知,BCL 中沒有內置方法可以執行此操作。

您應該添加一些初始空檢查,然后像在 BCL 中一樣重用它。

這幾乎肯定比這里給出的任何其他版本慢得多,但寫起來很有趣。

static bool ByteArrayEquals(byte[] a1, byte[] a2) 
{
    return a1.Zip(a2, (l, r) => l == r).All(x => x);
}

這與其他類似,但這里的區別在於沒有落到我可以一次檢查的下一個最高字節數,例如,如果我有 63 個字節(在我的 SIMD 示例中),我可以檢查第一個字節的相等性32 字節,然后是最后 32 字節,這比檢查 32 字節、16 字節、8 字節等要快。 您輸入的第一個檢查是您需要比較所有字節的唯一檢查。

這在我的測試中確實名列前茅,但僅差一點點。

下面的代碼正是我在 airbreath/ArrayComparePerf.cs 中測試它的方式。

public unsafe bool SIMDNoFallThrough()    #requires  System.Runtime.Intrinsics.X86
{
    if (a1 == null || a2 == null)
        return false;

    int length0 = a1.Length;

    if (length0 != a2.Length) return false;

    fixed (byte* b00 = a1, b01 = a2)
    {
        byte* b0 = b00, b1 = b01, last0 = b0 + length0, last1 = b1 + length0, last32 = last0 - 31;

        if (length0 > 31)
        {
            while (b0 < last32)
            {
                if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(b0), Avx.LoadVector256(b1))) != -1)
                    return false;
                b0 += 32;
                b1 += 32;
            }
            return Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(last0 - 32), Avx.LoadVector256(last1 - 32))) == -1;
        }

        if (length0 > 15)
        {
            if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(b0), Sse2.LoadVector128(b1))) != 65535)
                return false;
            return Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(last0 - 16), Sse2.LoadVector128(last1 - 16))) == 65535;
        }

        if (length0 > 7)
        {
            if (*(ulong*)b0 != *(ulong*)b1)
                return false;
            return *(ulong*)(last0 - 8) == *(ulong*)(last1 - 8);
        }

        if (length0 > 3)
        {
            if (*(uint*)b0 != *(uint*)b1)
                return false;
            return *(uint*)(last0 - 4) == *(uint*)(last1 - 4);
        }

        if (length0 > 1)
        {
            if (*(ushort*)b0 != *(ushort*)b1)
                return false;
            return *(ushort*)(last0 - 2) == *(ushort*)(last1 - 2);
        }

        return *b0 == *b1;
    }
}

如果沒有首選 SIMD,則將相同的方法應用於現有的 LongPointers 算法:

public unsafe bool LongPointersNoFallThrough()
{
    if (a1 == null || a2 == null || a1.Length != a2.Length)
        return false;
    fixed (byte* p1 = a1, p2 = a2)
    {
        byte* x1 = p1, x2 = p2;
        int l = a1.Length;
        if ((l & 8) != 0)
        {
            for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
                if (*(long*)x1 != *(long*)x2) return false;
            return *(long*)(x1 + (l - 8)) == *(long*)(x2 + (l - 8));
        }
        if ((l & 4) != 0)
        {
            if (*(int*)x1 != *(int*)x2) return false; x1 += 4; x2 += 4;
            return *(int*)(x1 + (l - 4)) == *(int*)(x2 + (l - 4));
        }
        if ((l & 2) != 0)
        {
            if (*(short*)x1 != *(short*)x2) return false; x1 += 2; x2 += 2;
            return *(short*)(x1 + (l - 2)) == *(short*)(x2 + (l - 2));
        }
        return *x1 == *x2;
    }
}

為此使用SequenceEquals進行比較。

如果您正在尋找一個非常快的字節數組相等比較器,我建議您看看這篇 STSdb Labs 文章: 字節數組相等比較器。 它具有一些用於字節 [] 數組相等性比較的最快實現,這些實現被呈現、性能測試和總結。

您還可以專注於這些實現:

BigEndianByteArrayComparer - 從左到右的快速字節 [] 數組比較器 (BigEndian) BigEndianByteArrayEqualityComparer - - 從左到右的快速字節 [] 相等比較器 (BigEndian) LittleEndianByteArrayComparer - 從右到左的快速字節 [] 數組比較器 (LittleEndian) LittleEndianByteArrayEqualityComparer - 快速字節[] 從右到左的相等比較器 (LittleEndian)

簡短的回答是這樣的:

    public bool Compare(byte[] b1, byte[] b2)
    {
        return Encoding.ASCII.GetString(b1) == Encoding.ASCII.GetString(b2);
    }

通過這種方式,您可以使用優化的 .NET 字符串比較來進行字節數組比較,而無需編寫不安全的代碼。 這是在后台完成的方式:

private unsafe static bool EqualsHelper(String strA, String strB)
{
    Contract.Requires(strA != null);
    Contract.Requires(strB != null);
    Contract.Requires(strA.Length == strB.Length);

    int length = strA.Length;

    fixed (char* ap = &strA.m_firstChar) fixed (char* bp = &strB.m_firstChar)
    {
        char* a = ap;
        char* b = bp;

        // Unroll the loop

        #if AMD64
            // For the AMD64 bit platform we unroll by 12 and
            // check three qwords at a time. This is less code
            // than the 32 bit case and is shorter
            // pathlength.

            while (length >= 12)
            {
                if (*(long*)a     != *(long*)b)     return false;
                if (*(long*)(a+4) != *(long*)(b+4)) return false;
                if (*(long*)(a+8) != *(long*)(b+8)) return false;
                a += 12; b += 12; length -= 12;
            }
       #else
           while (length >= 10)
           {
               if (*(int*)a != *(int*)b) return false;
               if (*(int*)(a+2) != *(int*)(b+2)) return false;
               if (*(int*)(a+4) != *(int*)(b+4)) return false;
               if (*(int*)(a+6) != *(int*)(b+6)) return false;
               if (*(int*)(a+8) != *(int*)(b+8)) return false;
               a += 10; b += 10; length -= 10;
           }
       #endif

        // This depends on the fact that the String objects are
        // always zero terminated and that the terminating zero is not included
        // in the length. For odd string sizes, the last compare will include
        // the zero terminator.
        while (length > 0)
        {
            if (*(int*)a != *(int*)b) break;
            a += 2; b += 2; length -= 2;
        }

        return (length <= 0);
    }
}

由於上面的許多花哨的解決方案不適用於 UWP,並且因為我喜歡 Linq 和函數式方法,所以我向您展示了我的版本來解決這個問題。 為了在出現第一個差異時逃避比較,我選擇了 .FirstOrDefault()

public static bool CompareByteArrays(byte[] ba0, byte[] ba1) =>
    !(ba0.Length != ba1.Length || Enumerable.Range(1,ba0.Length)
        .FirstOrDefault(n => ba0[n] != ba1[n]) > 0);

如果您有一個巨大的字節數組,您可以通過將它們轉換為字符串來比較它們。

你可以使用類似的東西

byte[] b1 = // Your array
byte[] b2 = // Your array
string s1 = Encoding.Default.GetString( b1 );
string s2 = Encoding.Default.GetString( b2 );

我使用過這個,我看到了巨大的性能影響。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM