教你使用.NET快速比較兩個(gè)byte數(shù)組是否相等
前言
之前在群里面有群友問(wèn)過(guò)一個(gè)這樣的問(wèn)題,在.NET中如何快速的比較兩個(gè)byte數(shù)組是否完全相等,聽(tīng)起來(lái)是一個(gè)比較兩個(gè)byte數(shù)組是完全相等是一個(gè)簡(jiǎn)單的問(wèn)題,但是深入研究以后,覺(jué)得還是有很多方案的,這里和大家一起分享下。
評(píng)測(cè)方案
這里為了評(píng)測(cè)不同方案的性能,我們用到了BenchmarkDotNet
這個(gè)庫(kù),這個(gè)庫(kù)目前已經(jīng)被收入.NET基金會(huì)下,BenchmarkDotNet
可以很方便的評(píng)測(cè)方法執(zhí)行的性能,支持幾乎所有的.NET運(yùn)行環(huán)境,并且能輸出詳細(xì)的報(bào)表。使用起來(lái)也非常簡(jiǎn)單,你只需要安裝BenchmakrDotNet
的Nuget包,然后使用其提供的類(lèi)和方法即可,這里是它的項(xiàng)目地址和幫助文檔。
我們通過(guò)BenchmarkDotNet來(lái)構(gòu)建一個(gè)這樣的評(píng)測(cè)用例.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using CompareByte; // 需要引入BenchmarkDotNet的命名空間 // 運(yùn)行Benchmark相當(dāng)簡(jiǎn)單,只需要執(zhí)行這個(gè)靜態(tài)方法,泛型是需要評(píng)測(cè)的類(lèi) var summary = BenchmarkRunner.Run<BenchmarkCompareMethod>(); // 我們需要一些評(píng)測(cè)內(nèi)存結(jié)果信息 // 并且生成HTML報(bào)表 [MemoryDiagnoser] [HtmlExporter] public class BenchmarkCompareMethod { // 準(zhǔn)備兩個(gè)數(shù)組,填充4MB大小的數(shù)據(jù) private static readonly byte[] XBytes = Enumerable.Range(0, 4096000).Select(c => (byte) c).ToArray(); private static readonly byte[] YBytes = Enumerable.Range(0, 4096000).Select(c => (byte) c).ToArray(); public BenchmarkCompareMethod() { // 修改數(shù)組最后一個(gè)元素,使其不同 XBytes[4095999] = 1; YBytes[4095999] = 2; } [Benchmark(Baseline = true)] public void ForCompare() { ..... } }
需要注意的是,為了保證評(píng)測(cè)的結(jié)果與生產(chǎn)環(huán)境一致,BenchmarkDotNet
是要求使用Release
模式運(yùn)行程序,這樣的話不僅代碼編譯成IL
時(shí)優(yōu)化,程序運(yùn)行中JIT
也會(huì)更加積極的參與生產(chǎn)機(jī)器碼優(yōu)化。需要在項(xiàng)目文件夾下面使用dotnet run -c Release
來(lái)運(yùn)行評(píng)測(cè)。
幾種不同的方案
For循環(huán)
一開(kāi)始看到這個(gè)需求,第一個(gè)想到的就是直接使用for
循環(huán)對(duì)byte[]
進(jìn)行按下標(biāo)比較,我覺(jué)得也是大家第一時(shí)間能想到的方案,那我們就上代碼跑跑看速度。
public static bool ForCompare(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; // 引用相等,可以直接認(rèn)為相等 if (x is null || y is null) return false; // 兩者引用不相等情況下,一方為null那就不相等 if (x.Length != y.Length) return false; // 兩者長(zhǎng)度不等,那么肯定也不相等 for (var index = 0; index < x.Length; index++) { if (x[index] != y[index]) return false; } return true; }
最終的結(jié)果如下所示,我們可以看到其實(shí)使用for
循環(huán)進(jìn)行比較是很快的,4MB大小的數(shù)組2ms左右就能比較完畢。
其實(shí)還有一個(gè)優(yōu)化點(diǎn),.NET的JIT
對(duì)一些方法默認(rèn)是不做inline
內(nèi)聯(lián)優(yōu)化的,這樣每次還有一個(gè)方法調(diào)用的開(kāi)銷(xiāo),我們讓jit
去積極的進(jìn)行內(nèi)聯(lián),再來(lái)試試。方法也很簡(jiǎn)單,只需要引入System.Runtime.CompilerServices
命名空間,然后在方法上面打上頭標(biāo)記即可。
要搞清楚為什么方法內(nèi)聯(lián)有用,首先要知道當(dāng)一個(gè)方法被調(diào)用的時(shí)候發(fā)生了什么
1、首先會(huì)有個(gè)執(zhí)行棧,存儲(chǔ)目前所有活躍的方法,以及它們的本地變量和參數(shù)2、當(dāng)一個(gè)新的方法被調(diào)用了,一個(gè)新的棧幀會(huì)被加到棧頂,分配的本地變量和參數(shù)會(huì)存儲(chǔ)在這個(gè)棧幀3、跳到目標(biāo)方法代碼執(zhí)行4、方法返回的時(shí)候,本地方法和參數(shù)會(huì)被銷(xiāo)毀,棧頂被移除5、返回原來(lái)地址執(zhí)行
這就是通常說(shuō)的方法調(diào)用的壓棧和出棧過(guò)程,因此,方法調(diào)用需要有一定的時(shí)間開(kāi)銷(xiāo)和空間開(kāi)銷(xiāo),當(dāng)一個(gè)方法體不大,但又頻繁被調(diào)用時(shí),這個(gè)時(shí)間和空間開(kāi)銷(xiāo)會(huì)相對(duì)變得很大,變得非常不劃算,同時(shí)降低了程序的性能。所以內(nèi)聯(lián)簡(jiǎn)單的說(shuō)就是把目標(biāo)方法里面代碼復(fù)制到調(diào)用方法的地方,無(wú)需壓棧、跳轉(zhuǎn)和出棧。
不過(guò)并不是所有的方法內(nèi)聯(lián)都有益處,需要方法體比較小,如果方法體很大的話在每一個(gè)調(diào)用的地方都會(huì)發(fā)生替換,浪費(fèi)內(nèi)存。
using System.Runtime.CompilerServices; ..... [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static bool ForCompare(byte[]? x, byte[]? y)
再來(lái)跑一下試試。
最后可以看到性能提升了30%呀,分配的字節(jié)數(shù)少了50% (雖然本來(lái)就只有2字節(jié)),講道理就可以直接交差了。
Memcmp
但是群里面還有小伙伴就不滿足了,有沒(méi)有其它的方案?有個(gè)小伙伴就跳出來(lái)說(shuō),操作系統(tǒng)是不是提供了類(lèi)似的功能?會(huì)不會(huì)使用C/C++代碼運(yùn)行起來(lái)會(huì)更加快速?
沒(méi)錯(cuò),操作系統(tǒng)確實(shí)提供了這樣的函數(shù),微軟提供了一個(gè)名為mscrt
(微軟C運(yùn)行時(shí)庫(kù))的庫(kù),里面就提到了memcmp
這個(gè)函數(shù)就可以來(lái)比較兩個(gè)buffer
是否相等。MSDN鏈接.
函數(shù)簽名是這樣的,這個(gè)函數(shù)位于mscrt.dll
內(nèi)。
int memcmp( const void *buffer1, // 數(shù)組1指針 const void *buffer2, // 數(shù)組2指針 size_t count // 比較字節(jié)數(shù) );
既然有現(xiàn)成的C語(yǔ)言代碼,那么C#應(yīng)該如何調(diào)用它呢?實(shí)際上C#經(jīng)常被大家成為C++++是有一定道理的,它在設(shè)計(jì)之初就考慮了和C、C++等代碼的交互。這里使用到了C#的Native Interop - P/Invoke
技術(shù),可以很方便的使用C風(fēng)格的ABI(C++、Rust等等都提供C語(yǔ)言ABI生成),在.NET底層大量的代碼都是通過(guò)這種方式和底層交互,有興趣的可以戳鏈接了解更詳細(xì)的信息。
那么如何使用它呢?以我們上面的函數(shù)為例,我們只需要引入System.Runtime.InteropServices
命名空間,然后按照上面memcmp
函數(shù)的簽名轉(zhuǎn)換為C#代碼就行了,最終的代碼如下所示。
using System; using System.Runtime.InteropServices; namespace CompareByte; public static class BytesCompare { [DllImport("msvcrt.dll")] // 需要使用的dll名稱(chēng) private static extern unsafe int memcmp(byte* b1, byte* b2, int count); // 由于指針使用是內(nèi)存不安全的操作,所以需要使用unsafe關(guān)鍵字 // 項(xiàng)目文件中也要加入<AllowUnsafeBlocks>true</AllowUnsafeBlocks>來(lái)允許unsafe代碼 public static unsafe bool MemcmpCompare(byte[]? x,byte[]? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; if (x.Length != y.Length) return false; // 在.NET程序的運(yùn)行中,垃圾回收器可能會(huì)整理和壓縮內(nèi)存,這樣會(huì)導(dǎo)致數(shù)組地址變動(dòng) // 所以,我們需要使用fixed關(guān)鍵字,將x和y數(shù)組'固定'在內(nèi)存中,讓GC不移動(dòng)它 // 更多詳情請(qǐng)看 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/fixed-statement fixed (byte* xPtr = x, yPtr = y) { return memcmp(xPtr, yPtr, x.Length) == 0; } } }
那我們來(lái)跑個(gè)分吧,看看結(jié)果怎么樣。
結(jié)果真的是Amazing呀,比我們使用的for
循環(huán)方案足足快了80+%,從原來(lái)需要1.7ms左右到現(xiàn)在只需要300us。
64字長(zhǎng)優(yōu)化
那是不是證明C#就是沒(méi)有C跑的那么快呢?C#那還有沒(méi)有優(yōu)化的空間呢?當(dāng)然是有方法的,實(shí)際上memcmp
使用的算法和我們現(xiàn)在用的不一樣。
我們知道衡量算法的時(shí)間復(fù)雜度是使用大O來(lái)表示,而這個(gè)其實(shí)是代碼執(zhí)行時(shí)間隨數(shù)據(jù)規(guī)模增長(zhǎng)的變化趨勢(shì)的一個(gè)體現(xiàn)。比如我輸入的數(shù)據(jù)量大小為n,完成這個(gè)函數(shù)我近似需要執(zhí)行n次,那么時(shí)間復(fù)雜度就是O(n)。
再來(lái)回到我們的問(wèn)題中,在最壞的情況下(x
和y
引用不相等且的長(zhǎng)度相等),我們上面寫(xiě)的ForCompare
就會(huì)進(jìn)入for
循環(huán)來(lái)遍歷x
和y
每一個(gè)元素進(jìn)行比較,所以它的時(shí)間復(fù)雜度就是O(n)
,那么問(wèn)題的關(guān)鍵就是如何降低它的時(shí)間復(fù)雜度。
一個(gè)數(shù)組它的地址空間是連續(xù)的,另外byte
類(lèi)型的長(zhǎng)度是8bit
,默認(rèn)比較方式就像下圖一樣,一個(gè)元素一個(gè)元素的比較,也就是每8bit
每8bit
進(jìn)行比較。
那我們能讓他一次比較更多的位數(shù)嗎?比如一次16位、32位、64位?當(dāng)然是可以的,畢竟我們現(xiàn)在基本都是64位的CPU,不嚴(yán)謹(jǐn)?shù)恼f(shuō)實(shí)際上CPU一次能處理64位數(shù)據(jù),那么我們?nèi)绾巫屗淮涡阅鼙容^64位呢?
有小伙伴就說(shuō),很簡(jiǎn)單嘛,byte
是8bit
,我們直接用long
不就有64bit
了嗎?沒(méi)錯(cuò),就是這么簡(jiǎn)單,我們可以把byte*
指針強(qiáng)轉(zhuǎn)為long*
指針,然后一次性比較64位,如下圖所示。
上代碼(我這用的是UInt64
包裝的ulong
,一樣是64位,沒(méi)有符號(hào)位會(huì)更快一點(diǎn)):
public static unsafe bool UlongCompare(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; if (x.Length != y.Length) return false; fixed (byte* xPtr = x, yPtr = y) { return UlongCompareInternal(xPtr, yPtr, x.Length); } } private static unsafe bool UlongCompareInternal(byte* xPtr, byte* yPtr, int length) { // 指針+偏移量計(jì)算出數(shù)組最后一個(gè)元素地址 byte* lastAddr = xPtr + length; byte* lastAddrMinus32 = lastAddr - 32; while (xPtr < lastAddrMinus32) // 我們一次循環(huán)比較32字節(jié),也就是256位 { // 一次判斷比較前64位 if (*(ulong*) xPtr != *(ulong*) yPtr) return false; // 第二次從64為開(kāi)始,比較接下來(lái)的64位,需要指針偏移64位,一個(gè)byte指針是8為,所以需要偏移8個(gè)位置才能到下一輪起始位置 // 所以代碼就是xPtr+8 if (*(ulong*) (xPtr + 8) != *(ulong*) (yPtr + 8)) return false; // 同上面一樣,第三次從第128位開(kāi)始比較64位 if (*(ulong*) (xPtr + 16) != *(ulong*) (yPtr + 16)) return false; // 第四次從第192位開(kāi)始比較64位 if (*(ulong*) (xPtr + 24) != *(ulong*) (yPtr + 24)) return false; // 一輪總共比較了256位,讓指針偏移256位 xPtr += 32; yPtr += 32; } // 因?yàn)樯厦媸且淮涡员容^32字節(jié)(256位),可能數(shù)組不能為32整除,最后只留下比如30字節(jié),20字節(jié) // 最后的幾個(gè)字節(jié),我們用循環(huán)來(lái)逐字節(jié)比較 while (xPtr < lastAddr) { if (*xPtr != *yPtr) return false; xPtr++; yPtr++; } return true; }
那我們來(lái)跑個(gè)分吧。
可以看到基本和memcmp
打平了,幾u(yù)s的差別可以看做是誤差。大佬們一直說(shuō),C#是一門(mén)下限低,上限高的語(yǔ)言,你開(kāi)心的話寫(xiě)出來(lái)的代碼完全媲美C++,代碼里面還能嵌入?yún)R編,只是有點(diǎn)麻煩,O(∩_∩)O哈哈~
SIMD
那么我們就這樣滿足了嗎?
小伙伴又問(wèn)了,既然我們可以一次性比較64位,那我們能比較更多的位數(shù)嗎?比如128位,256位?答案是當(dāng)然可以,這個(gè)是CPU的一個(gè)技術(shù),叫Single Instruction Multiple Data,簡(jiǎn)稱(chēng)為SIMD,SIMD主要就是說(shuō)CPU中可以單條指令實(shí)現(xiàn)數(shù)據(jù)的并行處理,這類(lèi)指令在數(shù)字信號(hào)處理、圖像處理、以及多媒體信息處理等領(lǐng)域非常有效。
我們打開(kāi)CPU-Z,可以看到指令集有很多,這都是CPU為了特殊的程序單獨(dú)做的優(yōu)化。
MMX:MMX 是MultiMedia eXtensions(多媒體擴(kuò)展)的縮寫(xiě),是第六代CPU芯片的重要特點(diǎn)。MMX技術(shù)是在CPU中加入了特地為視頻信號(hào)(Video Signal),音頻信號(hào)(Audio Signal)以及圖像處理(Graphical Manipulation)而設(shè)計(jì)的57條指令,因此,MMX CPU極大地提高了電腦的多媒體(如立體聲、視頻、三維動(dòng)畫(huà)等)處理功能。
SSE:SSE是 “因特網(wǎng)數(shù)據(jù)流單指令序列擴(kuò)展 ( Internet Streaming SIMD Extensions)的縮寫(xiě)。SSE除保持原有的MMX指令外,又新增了70條指令,在加快浮點(diǎn)運(yùn)算的同時(shí),改善了內(nèi)存的使用效率,使內(nèi)存速度更快,后面有一些增強(qiáng)版SSE2、SSE3等等。
EM64T:Intel的EM64T技術(shù),EM64T技術(shù)官方全名是Extended Memory 64 Technology,中文解釋就是擴(kuò)展64bit內(nèi)存技術(shù)。
AES:AES(Advanced Encryption Standard,高級(jí)加密標(biāo)準(zhǔn))指令集,是專(zhuān)門(mén)為加密解密設(shè)計(jì)的,與此前相比AES加密/解密之性能高出3倍。
AVX:Advanced Vector eXtentions(AVX)在2008年由Intel與AMD提出,并于2011年分別在Sandy Bridge以及Bulldozer架構(gòu)上提供?持。AVX的主要改進(jìn)在于對(duì)寄存器長(zhǎng)度的擴(kuò)展以及提供了更靈活的指令集。 AVX對(duì) XMM 寄存器做了擴(kuò)展,從原來(lái)的128-bit擴(kuò)展到了256-bit,256-bit的寄存器命名為 YMM 。YMM的低128-bit是與XMM混? 的。
對(duì)于這些指令集,在.NET上提供了System.Runtime.Intrinsics.X86
命名空間,其中支持了各種指令集原生的訪問(wèn),想了解更多的東西,可以戳這個(gè)鏈接。由于SIMD在.NET上有著天然的支持,可以很方便的寫(xiě)出SIMD代碼,而其它編程語(yǔ)言平臺(tái)或多或少支持都不是很完美。
類(lèi)名 | 作用 |
---|---|
Aes | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel AES 硬件指令的訪問(wèn)權(quán)限。 |
Avx | 該類(lèi)通過(guò)內(nèi)聯(lián)函數(shù)提供對(duì) Intel AVX 硬件指令的訪問(wèn)權(quán)限。 |
Avx2 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel AVX2 硬件指令的訪問(wèn)。 |
Bmi1 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel BMI1 硬件指令的訪問(wèn)權(quán)限。 |
Bmi2 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel BMI2 硬件指令的訪問(wèn)權(quán)限。 |
Fma | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel FMA 硬件指令的訪問(wèn)權(quán)限。 |
Lzcnt | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel LZCNT 硬件指令的訪問(wèn)權(quán)限。 |
Pclmulqdq | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel PCLMULQDQ 硬件指令的訪問(wèn)權(quán)限。 |
Popcnt | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel POPCNT 硬件指令的訪問(wèn)權(quán)限。 |
Sse | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel SSE 硬件指令的訪問(wèn)權(quán)限。 |
Sse2 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel SSE2 硬件指令的訪問(wèn)權(quán)限。 |
Sse3 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel SSE3 硬件指令的訪問(wèn)權(quán)限。 |
Sse41 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel SSE 4.1 硬件指令的訪問(wèn)。 |
Sse42 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel SSE4.2 硬件指令的訪問(wèn)權(quán)限。 |
Ssse3 | 此類(lèi)通過(guò)內(nèi)部函數(shù)提供對(duì) Intel SSSE3 硬件指令的訪問(wèn)權(quán)限。 |
X86Base | 通過(guò)內(nèi)部函數(shù)提供對(duì) x86 基本硬件指令的訪問(wèn)。 |
Sse
我們看到SSE系列的指令集可以操作128位,那我們就來(lái)試試128位會(huì)不會(huì)更快一些,直接上代碼。
using System.Runtime.Intrinsics.X86; // 需要引入這個(gè)命名空間 namespace CompareByte; public static class BytesCompare { ...... public static unsafe bool Sse2Compare(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; if (x.Length != y.Length) return false; fixed (byte* xPtr = x, yPtr = y) { return Sse2CompareInternal(xPtr, yPtr, x.Length); } } private static unsafe bool Sse2CompareInternal(byte* xPtr, byte* yPtr, int length) // 這里的算法與64位大體一樣,只是位數(shù)變成了128位 byte* lastAddr = xPtr + length; byte* lastAddrMinus64 = lastAddr - 64; const int mask = 0xFFFF; while (xPtr < lastAddrMinus64) // 使用Sse2.LoadVector128()各加載x和y的128位數(shù)據(jù) // 再使用Sse2.CompareEqual()比較是否相等,它的返回值是一個(gè)128位向量,如果相等,該位置返回0xffff,否則返回0x0 // CompareEqual的結(jié)果是128位的,我們可以通過(guò)Sse2.MoveMask()來(lái)重新排列成32位,最終看是否等于0xffff就好 if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(xPtr), Sse2.LoadVector128(yPtr))) != mask) { return false; } if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(xPtr + 16), Sse2.LoadVector128(yPtr + 16))) != mask) if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(xPtr + 32), Sse2.LoadVector128(yPtr + 32))) != mask) if (Sse2.MoveMask(Sse2.CompareEqual(Sse2.LoadVector128(xPtr + 48), Sse2.LoadVector128(yPtr + 48))) != mask) xPtr += 64; yPtr += 64; while (xPtr < lastAddr) if (*xPtr != *yPtr) return false; xPtr++; yPtr++; return true; }
放到JIT里面看看,有沒(méi)有生成SIMD代碼,可以明顯的看到匯編代碼里面已經(jīng)有了SIMD代碼。
來(lái)看看跑分結(jié)果。
可以看到對(duì)比memcmp
的方式快了2+%,那按道理來(lái)說(shuō)從64位到128位應(yīng)該快50%左右才對(duì),為什么只快了2+%呢?
其實(shí)這是因?yàn)镾IMD是單條指令多數(shù)據(jù)處理,其中運(yùn)算還是CPU內(nèi)部的64位單元處理,只是少了多條指令的開(kāi)銷(xiāo)。另外是因?yàn)樵?4位是只比較了一次,而SIMD需要經(jīng)歷CompareEqual
、MoveMask
最后還需和mask
掩碼比較,總共次數(shù)多了2次。只能說(shuō)明在我們的這個(gè)場(chǎng)景下,提升會(huì)比較有限。
需要注意目標(biāo)平臺(tái)需要能支持這些特殊的指令集,可以通過(guò)Sse2.IsSupported
方法來(lái)判斷。
Avx2
既然128位的SSE系列指令集能在原來(lái)的基礎(chǔ)上提升2%,那我們來(lái)看看支持256位的Avx2指令集會(huì)提升多少。代碼和SSE指令集幾乎一樣,只是調(diào)用的方法類(lèi)名變動(dòng)了。
using System.Runtime.Intrinsics.X86; // 需要引入這個(gè)命名空間 namespace CompareByte; public static class BytesCompare { ...... public static unsafe bool Avx2Compare(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; if (x.Length != y.Length) return false; fixed (byte* xPtr = x, yPtr = y) { return Avx2CompareInternal(xPtr, yPtr, x.Length); } } private static unsafe bool Avx2CompareInternal(byte* xPtr, byte* yPtr, int length) byte* lastAddr = xPtr + length; byte* lastAddrMinus128 = lastAddr - 128; const int mask = -1; while (xPtr < lastAddrMinus128) // 更換為Avx2指令集,一次加載256位 if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(xPtr), Avx.LoadVector256(yPtr))) != mask) { return false; } if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(xPtr + 32), Avx.LoadVector256(yPtr + 32))) != mask) if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(xPtr + 64), Avx.LoadVector256(yPtr + 64))) != mask) if (Avx2.MoveMask(Avx2.CompareEqual(Avx.LoadVector256(xPtr + 96), Avx.LoadVector256(yPtr + 96))) != mask) xPtr += 128; yPtr += 128; while (xPtr < lastAddr) if (*xPtr != *yPtr) return false; xPtr++; yPtr++; return true; }
再來(lái)看看跑分結(jié)果。
可以看到,Avx2指令集對(duì)于memcmp
和Sse2
是有一定的提升的,有2+%左右的速度提升,另外相較于原本的for
循環(huán)比較提升了86%。
SequenceCompare
那么是不是以后我們寫(xiě)比較兩個(gè)數(shù)組相等的代碼都要寫(xiě)這一長(zhǎng)串的unsafe代碼呢?其實(shí)并不是,在.NET Core時(shí)代引入了Span這個(gè)特性,這個(gè)特性就是為了能安全的直接操作內(nèi)存;與此同時(shí),也提供了SequenceEquals
方法,能快速的比較兩個(gè)序列,使用也非常簡(jiǎn)單,那究竟性能怎么樣呢?我們上代碼,跑個(gè)分。
// 代碼非常簡(jiǎn)單,只需要調(diào)用System.Linq.SequenceEqual方法即可 public static bool SequenceCompare(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; if (x.Length != y.Length) return false; return x.SequenceEqual(y); }
結(jié)果也是相當(dāng)不錯(cuò)的,比memcmp
和SSE2的方式都要快一點(diǎn),略遜于Avx2,但是它用起來(lái)很簡(jiǎn)單,那么它是如何做到這么快的呢?讓我們看看它的源碼,
鏈接貌似也沒(méi)有什么技巧,那是不是JIT編譯的時(shí)候有優(yōu)化,給自動(dòng)向量化了呢?我們將代碼復(fù)制出來(lái),然后單獨(dú)跑了一下,再用WinDBG打開(kāi),我們可以看到確實(shí)JIT優(yōu)化引入了一些自動(dòng)向量化(SIMD)的操作。
總結(jié)
通過(guò)這幾種方案的對(duì)比,最推薦的用法當(dāng)然就是直接使用.NET庫(kù)提供的SequenceEquals
方法來(lái)完成比較,如果是在.NET Framework中,由于沒(méi)有這樣的優(yōu)化,所以大家也可以嘗試上文中提到的SSE2等方法。
那么大家還有什么其它好的方式呢?歡迎在評(píng)論區(qū)留言!
筆者水平有限,如有錯(cuò)漏請(qǐng)批評(píng)指正 :)
本文源碼鏈接
參考文獻(xiàn)
Checking equality for two byte arrays
到此這篇關(guān)于.NET如何快速比較兩個(gè)byte數(shù)組是否相等的文章就介紹到這了,更多相關(guān).NET比較兩個(gè)byte數(shù)組是否相等內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
.Net使用Cancellation?Framework取消并行任務(wù)
這篇文章介紹了.Net使用Cancellation?Framework取消并行任務(wù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06asp.net core為IHttpClientFactory添加動(dòng)態(tài)命名配置
某些時(shí)候我們需要為HttpClient動(dòng)態(tài)配置一些東西, 例如證書(shū)等, 例如服務(wù)是一個(gè)回調(diào)服務(wù), 而被回調(diào)方采用了自定義的https(即自定義證書(shū)),本文就將講述如何實(shí)現(xiàn)這種需求2021-06-06ASP.NET 文件斷點(diǎn)續(xù)傳實(shí)現(xiàn)代碼
在文件下載的時(shí)候,使用斷點(diǎn)續(xù)傳可以將上次未下載完成的文件繼續(xù)下載,該功能在開(kāi)發(fā)文件下載的時(shí)候非常重要。這里我將介紹一種比較簡(jiǎn)單的斷點(diǎn)續(xù)傳功能的實(shí)現(xiàn)方法,僅供初學(xué)者參考使用2012-06-06ASP.NET中的跳轉(zhuǎn) 200, 301, 302轉(zhuǎn)向?qū)崿F(xiàn)代碼
跳轉(zhuǎn)非常常用,在哪里都一樣,這里的一些說(shuō)明和用法也如此,不止適用于asp.net,其他語(yǔ)言也會(huì)用得到。跳轉(zhuǎn)的目的本來(lái)很簡(jiǎn)單,就是當(dāng)用戶或系統(tǒng)需要時(shí)從一個(gè)頁(yè)面轉(zhuǎn)向另一個(gè)頁(yè)面,但自從有了各種各樣的需求,還有那個(gè)什么SEO的東西之后,跳轉(zhuǎn)被搞得極其復(fù)雜2008-09-09VS2005 水晶報(bào)表在時(shí)部署時(shí)遇到的問(wèn)題
前幾天在服務(wù)器上部署一個(gè)B/S程序的時(shí)候,程序中的水晶報(bào)表部分出了些問(wèn)題,報(bào)錯(cuò):Server Error in '/' Application.2010-02-02