.NET中的两个字节数组比较


541

我该如何快速完成?

当然,我可以这样做:

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。

在这里记下我的超快速回答。


1
“这有赖于数组以qword开始对齐的事实。” 那是很大的。您应该修复代码以反映这一点。
乔钟

4
返回a1.Length == a2.Length &&!a1.Where((t,i)=> t!= a2 [i])。Any();
alerya

我喜欢@OhadSchneider回答IStructuralEquatable
LCJ

Answers:


613

您可以使用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,则方法可以。
编译器\运行时环境将优化您的循环,因此您无需担心性能。


4
但是,SequenceEqual是否需要比不安全的比较花费更长的时间?特别是当您进行1000次比较时?
tcables 2011年

90
是的,这比不安全的比较慢了约50倍。
Hafthor

27
这确实在这里使死人复活,但是在这里使用“慢”确实是一个坏词。慢50倍听起来很糟糕,但是您经常不需比较足够的数据以产生影响,如果确实如此,出于多种原因,您确实需要针对自己的情况进行基准测试。例如,请注意,不安全答案的创建者注意,其速度相差7倍,而不是慢50倍(不安全方法的速度还取决于数据的对齐方式)。在这些数字很重要的极少数情况下,P / Invoke甚至更快。
Selali Adob​​or 2014年

4
那么执行速度较慢的用户会获得300多个赞?我建议挂上msvcrt.dll,因为这将是完成工作的最快方法。
TGarrett,2015年

69
对于企业而言,最快不是最重要的事情。可维护性比“在99%的情况下节省此代码要快得多”。我正在使用SequenceEqual,并且我的整个代码都小于1毫秒。您所节省的µs永远不会等于P / Invoke缺乏可读性的5分钟之久。
PRMan 2015年

236

P /调用电源激活!

[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;
}

48
的P / Invoke yaay -这被证明是迄今为止对位图至少是最快的:stackoverflow.com/questions/2031217/...
埃里克·福布斯

25
在这种情况下,不需要固定。当使用PInvoke调用本机代码时,编组器执行自动固定。参考:stackoverflow.com/questions/2218444/...
马克格拉斯哥

14
P / Invoke可能会引起嘘声,但这是目前所有解决方案中最快的,包括我提出的使用不安全的指针大小比较的实现。在调出本机代码之前,您可以进行一些优化,包括引用相等性以及比较第一个和最后一个元素。
乔什(Josh)

38
为什么嘘?Poster希望快速实施,并且无法比拟优化的汇编语言。我不知道如何在没有P / INVOKE的情况下从.NET中获取“ REPE CMPSD”。
詹森·古玛

14
Nitpick:用户代码不应使用MSVCR.dll。要使用MSVCR,您必须使用所分发的版本来分发运行时。(msdn.microsoft.com/en-us/library/...blogs.msdn.com/b/oldnewthing/archive/2014/04/11/10516280.aspx
米奇

160

.NET 4中为此提供了一个新的内置解决方案-IStructuralEquatable

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

17
根据这篇博客文章,这实际上非常缓慢。
马特·约翰逊·品脱

48
疯狂慢。比简单的for循环慢约180倍。
Hafthor 2012年

这行得通,但是我不明白为什么。byte []是未实现IStructuralEquatable的原始类型,那么为什么要强制转换它以及隐式转换呢!然后,接口“ Equals”方法神奇地变为可用...该方法的实现来自哪里?有人可以告诉我吗?
2013年

1
为什么不只是StructuralComparisons.StructuralEqualityComparer.Equals(a1, a2)NullReferenceException这里没有。
ta.speot。是2014年

1
@ ta.speot.is谢谢,不能与一个班轮争论!以前的解决方案效率更高,因为它已将强制转换保存到IStructuralEquatable(一个数组在静态上称为IStructuralEquatable),但实际上您的建议也使该方法也适用于空参数。
Ohad Schneider 2014年

76

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循环执行大约七个计时器。使用J#库等效于原始for循环执行。使用.SequenceEqual大约慢7倍;我认为只是因为它正在使用IEnumerator.MoveNext。我想象基于LINQ的解决方案至少要这么慢或更糟。


3
不错的解决方案。但是有一个(小的)提示:如果引用a1和a2相等,则进行比较可能会加快速度,如果对a1和b1给出相同的数组。
mmmmmmmm 2012年

12
在.NET 4 x64版本上发布的新测试数据:IStructualEquatable.equals慢180倍,SequenceEqual慢15倍,SHA1哈希比较慢11倍,bitconverter相同,不安全7倍,pinvoke快11倍。很酷的是,不安全仅比memcmp上的P / Invoke慢一点。
Hafthor 2012年

3
该链接提供了有关为何内存对齐为何重要的详细信息ibm.com/developerworks/library/pa-dalign-因此,一种优化可能是检查对齐,并且如果两个数组的偏移量相同,则进行字节比较,直到它们都对齐在qword边界上。
Hafthor

5
当a1和a2都为null时,这不会给出false吗?
nawfal

2
@CristiDiaconescu我问了KevinDriedger的答案。我可能应该做的是使测试套件和结果在github上可用,并在我的答案上链接到它。
Hafthor

73

Span<T> 提供了极具竞争力的替代方案,而不必在您自己的应用程序的代码库中添加混乱和/或不可移植的绒毛:

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

可以在此处找到.NET Core 3.1.0以来的实现。

修改了 @EliArbel的要点,将其添加为SpansEqual,将大多数不那么有趣的执行器放入其他基准测试中,以不同的数组大小运行它,输出图形,并将其标记SpansEqual为基线,以便它报告不同方法的比较结果。SpansEqual

下面的数字来自结果,对其进行了轻轻地编辑以删除“错误”列。

|        Method |  ByteCount |               Mean |            StdDev | Ratio |
|-------------- |----------- |-------------------:|------------------:|------:|
|    SpansEqual |         15 |           3.562 ns |         0.0035 ns |  1.00 |
|  LongPointers |         15 |           4.611 ns |         0.0028 ns |  1.29 |
|      Unrolled |         15 |          18.035 ns |         0.0195 ns |  5.06 |
| PInvokeMemcmp |         15 |          11.210 ns |         0.0353 ns |  3.15 |
|               |            |                    |                   |       |
|    SpansEqual |       1026 |          20.048 ns |         0.0286 ns |  1.00 |
|  LongPointers |       1026 |          63.347 ns |         0.1062 ns |  3.16 |
|      Unrolled |       1026 |          39.175 ns |         0.0304 ns |  1.95 |
| PInvokeMemcmp |       1026 |          40.830 ns |         0.0350 ns |  2.04 |
|               |            |                    |                   |       |
|    SpansEqual |    1048585 |      44,070.526 ns |        35.3348 ns |  1.00 |
|  LongPointers |    1048585 |      59,973.407 ns |        80.4145 ns |  1.36 |
|      Unrolled |    1048585 |      55,032.945 ns |        24.4745 ns |  1.25 |
| PInvokeMemcmp |    1048585 |      55,593.719 ns |        22.4301 ns |  1.26 |
|               |            |                    |                   |       |
|    SpansEqual | 2147483591 | 253,648,180.000 ns | 1,112,524.3074 ns |  1.00 |
|  LongPointers | 2147483591 | 249,412,064.286 ns | 1,079,409.5670 ns |  0.98 |
|      Unrolled | 2147483591 | 246,329,091.667 ns |   852,021.7992 ns |  0.97 |
| PInvokeMemcmp | 2147483591 | 247,795,940.000 ns | 3,390,676.3644 ns |  0.98 |

我很惊讶地发现SpansEqualmax-array-size方法没有脱颖而出,但是差异是如此之小,以至于我认为这无关紧要。

我的系统信息:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.100
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
  DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT

我从没想过我会在所有工作中使用Span <T>或接近它的东西。多亏了您,我现在可以向我的同事们吹牛。
jokab

SequenceEqual是否特别作为Span方法实现?以为这只是IEnumerable扩展方法之一。
Zastai

1
@Zastai是的,{ReadOnly,}Span<T>具有自己的版本SequenceEqual(名称相同,因为它与相应的IEnumerable<T>扩展方法具有相同的约定,只是速度更快)。请注意,由于类型的限制,{ReadOnly,}Span<T>不能使用IEnumerable<T>扩展方法ref struct
Joe Amenta '18

1
@ Sentinel,System.Memory程序包具有“便携式” /“慢速” Span<T>实现netstandard1.1以及更高版本(因此,请使用此交互式图表来查看它们)。目前,“快速” Span<T>仅在.NET Core 2.1中可用,但请注意,SequenceEqual<T>具体来说,“快速”和“慢速” /“便携式”之间应该没有什么区别(尽管netstandard2.0目标应该有所改善,因为它们具有向量化的代码路径)。
Joe Amenta

1
install-package system.memory
Chris Moschini,

30

如果您不反对这样做,则可以导入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;
}

25

.NET 3.5及更高版本具有一个新的公共类型,System.Data.Linq.Binary它封装了byte[]。它实现IEquatable<Binary>了(实际上)比较两个字节数组。请注意,System.Data.Linq.Binary还具有来自的隐式转换运算符byte[]

MSDN文档: System.Data.Linq.Binary

等于方法的反射器反编译:

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对象的构造函数中计算哈希值为代价的(通过使用forloop :-) 遍历数组)。

上面的实现意味着在最坏的情况下,您可能必须遍历三个数组:首先计算array1的哈希值,然后计算array2的哈希值,最后(因为这是最坏的情况,长度和哈希值相等)进行比较array1中的字节与array 2中的字节。

总的来说,即使System.Data.Linq.Binary内置在BCL中,我也不认为这是比较两个字节数组:-|的最快方法。


20

我发布了一个类似的问题,用于检查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

1
我确定。我也进行了测试。这比使用memcmp不安全呼叫的答案要快。
ujeenator 2015年

1
@AmberdeBlack您确定吗?您是否使用小型阵列进行了测试?
Zar Shardan

@ArekBulski您确定这比memcmp快,否则会导致我的测试显示吗?
Zar Shardan

在此和memcmp之间,我获得了几乎相同的性能,因此,对于完全托管的解决方案,请+1。
Mike Marynowski

10
 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");
 }

1
这就是我一直在使用的。但这听起来...听起来像是一个顺序比较,否则您将使用一个简单的循环来进行比较,因此速度不是很快。反映这一点并查看实际操作将非常高兴。从名字来看,没什么特别的。
谢尔盖·阿科波夫

1
是的,但已在接受的答案中提及。顺便说一句,您可以在此处删除类型规范。
nawfal 2013年

10

让我们再添加一个!

最近,Microsoft发布了一个特殊的NuGet包System.Runtime.CompilerServices.Unsafe。它之所以特别是因为它是用IL编写的,并且提供了C#中不直接可用的低级功能。

它的一种方法Unsafe.As<T>(object)允许将任何引用类型转换为另一引用类型,从而跳过任何安全检查。这通常是一个非常糟糕的主意,但是如果两种类型都具有相同的结构,则可以使用。因此,我们可以使用它将a强制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 |

我还通过所有测试创建了要点


它不使用unsafe关键字,但是无论如何它都会通过使用System.Runtime.CompilerServices.Unsafe调用不安全的代码
Paulo Zemek '18

我已经更新了我的NewMemCmp答案,使用AVX-2
安德森

8

我开发了一种在我的PC memcmp()上略微跳动(EqualBytesLongUnrolled()基音的答案)而非常缓慢地跳动(Arek Bulski的答案)的方法。基本上,它将展开循环而不是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 462,我的测量结果有所不同:
NETCORE

比较两个长度为0的数组时,代码会崩溃,因为固定返回null
Glenn Slayden '17

memcmp不仅仅是一个股票比较器。它提供有关哪个对象更大或更小的信息。您可以为此目的采用算法并检查性能吗?
nicolay.anykienko,

它比Span和快memcpy吗?
Silkfire

@silkfire在.NET Core 3和现代CPU上,大型阵列的速度应提高2-3倍。
安德森先生

6

我将使用不安全的代码并for比较Int32指针运行循环。

也许您还应该考虑检查数组是否为空。


5

如果您看一下.NET如何处理string.Equals,您会发现它使用了一个称为EqualsHelper的私有方法,该方法具有“不安全”的指针实现。.NET Reflector是您的朋友,可以了解内部的工作方式。

这可以用作字节数组比较的模板,我在博客文章C#中的快速字节数组比较中做了一个实现。我还做了一些基本的基准测试,以了解安全实施的时间快于不安全实施的时间。

就是说,除非您确实需要杀手级的性能,否则我将进行简单的fr循环比较。


3

找不到我完全满意的解决方案(合理的性能,但没有不安全的代码/密码),所以我想到了这个,虽然没有什么真正的原创性,但可行:

    /// <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

不安全比较:1636滴答,12.12

EqualBytesLongUnrolled:637 ticks,31.09

P /调用memcmp:369 ticks,53.67

在linqpad中进行了测试,具有1000000个字节的相同数组(最坏的情况),每个数组有500次迭代。


是的,我注意到在stackoverflow.com/a/1445280/4489的评论中,我的测试表明这实际上比我在原始问题中使用的简单for循环要慢一些。
哈夫索

你确定吗?在我的测试中,它快4倍吗?即使有封送处理开销,也没有什么比老式的本机代码更好。
Zar Shardan

3

看起来 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 |

我已经更新了我的NewMemCmp答案,使用AVX-2
安德森

3

我在这里没有看到很多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();
 }

问题的重点是该函数在问题中发布的更快的解决方案。
CodesInChaos

3

我使用未附加调试器的附加程序.net 4.7版本进行了一些测量。我认为人们一直使用错误的度量标准,因为如果您关心这里的速度,那是要弄清楚两个字节数组是否相等需要花费多长时间。即吞吐量(以字节为单位)。

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这更好的方法了,而且速度要快几个数量级。一个简单的for循环是第二好的选择。而且,这仍然让我感到困惑,为什么Microsoft不能简单地包含一种Buffer.Compare方法。

[Program.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);
            });
        }
    }
}

2

为了比较短字节数组,以下是一个有趣的技巧:

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); 

然后,我可能会遇到问题中列出的解决方案。

对此代码进行性能分析将很有趣。


int i = 0; for(; i <a1.Length-7; i + = 8)if(BitConverter.ToInt64(a1,i)!= BitConverter.ToInt64(a2,i))返回false; for(; i <a1.Length; i ++)if(a1 [i]!= a2 [i])返回false; 返回true;//比简单的for循环慢一点。
Hafthor 2012年

2

对于那些关心订单的人(即希望您memcmp返回的int东西而不是什么都不要),. NET Core 3.0(以及大概的.NET Standard 2.1或.NET 5.0)将包括一个Span.SequenceCompareTo(...)扩展方法(加a Span.SequenceEqualTo),用于比较两个ReadOnlySpan<T>实例(where T: IComparable<T>)。

最初的GitHub提案中,讨论包括与跳转表计算的方法比较,读取byte[]as long[],SIMD用法以及对CLR实现的p /调用memcmp

展望未来,这应该是比较字节数组或字节范围的首选方法(应该使用.NET Standard 2.1 API Span<byte>代替byte[]),并且它的速度足够快,您不再需要对其进行优化(和否,尽管名称相似,但效果不如horrid 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

1

我想到了许多图形卡内置的块传输加速方法。但是随后您将不得不按字节复制所有数据,因此如果您不想以不受管且与硬件相关的代码来实现逻辑的整个部分,那么这对您没有多大帮助。

与上面显示的方法类似的另一种优化方法是,从一开始就将尽可能多的数据存储在long []中,而不是byte []中,例如,如果您从二进制文件中顺序读取数据,或者,如果您使用内存映射文件,则将数据读取为long []或单个long值。然后,您的比较循环将只需要对包含相同数据量的byte []执行的迭代次数的1/8。您需要何时以及多长时间比较一次,以及何时以及多长时间需要按字节访问数据的问题,例如,在API调用中将其用作期望的方法中的参数一个字节[]。最后,您只能告诉您是否真的知道用例...


接受的答案将字节缓冲区重铸为长缓冲区,并按照您的描述进行比较。
Hafthor 2012年

1

几乎可以肯定,这比这里给出的任何其他版本都要慢得多,但是编写起来很有趣。

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

1

我选择了一个受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;
}

0

抱歉,如果您正在寻找一种托管方式,那么您已经在正确地进行此操作,据我所知,BCL中没有内置方法可以执行此操作。

您应该添加一些初始的空检查,然后就像在BCL中一样重复使用它。


您写的时候是对的,但是在2010年(.NET 4.0)中出现了BCL方法,请参见Ohad Schneider的答案。在提出问题时,.NET 3.5使用了Linq(请参阅aku的回答)。
杰普·斯蒂格·尼尔森


-2

如果您正在寻找一个非常快速的字节数组相等比较器,建议您看一下这篇STSdb Labs文章:字节数组相等比较器。它具有一些用于byte []数组相等比较的最快实现,可以对它们进行介绍,性能测试和总结。

您还可以关注以下实现:

BigEndianByteArrayComparer-从左至右的快速byte []数组比较器(BigEndian)BigEndianByteArrayEqualityComparer--从左至右的 快速byte []相等比较器(BigEndian) LittleEndianByteArrayComparer-从右至左的快速byte []数组比较器(LittleEndian) LittleEndianByteArrayEqualityComparer-快速字节[]从右到左的相等比较器(LittleEndian)


-2

简短的答案是这样的:

    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);
    }
}

在我的测试中,转换为字符串破坏了比较快的优势。这比简单的for循环慢约2.5倍。
Doug Clutter 2015年

当我做同样的事情时,简单的速度要慢大约8倍。您可以在这里编写代码吗?
阿隆2015年

1
如果一个字节包含一个空(0)值,这会中断吗?
Joseph Lennox

-1除了由于@DougClutter指出的转换为字符串而变慢之外,如果字节数组包含非ASCII数据,则失败。为了获得正确的结果,将需要使用iso-8859-1。

2
Compare(new byte[]{128}, new byte[]{ 255 }) == true根本不是越野车...
CodesInChaos

-2

由于上述许多出色的解决方案均不适用于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);

-1,因为此代码已损坏并且显然未经测试。这将抛出一个IndexOutOfRangeException比较非空数组时,因为你所访问的元素1通过ba0.Length时,它应该0通过ba0.Length - 1。如果用Enumerable.Range(0, ba0.Length)它修复它,仍然错误地返回true相等长度的数组,其中只有第一个元素不同,因为您不能区分满足的第一个元素predicate没有满足的元素predicate; 在两种情况下都FirstOrDefault<int>()返回0
培根

这里的孩子们的教训:不要带刀进行枪战
理查德·豪尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.