如何使用.NET在2.2秒內(nèi)處理10億行數(shù)據(jù)(1brc挑戰(zhàn))
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
譯者注#在上周我就關(guān)注到了在github上有1brc這樣一個(gè)挑戰(zhàn),當(dāng)時(shí)看到了由Victor Baybekov提交了.NET下最快的實(shí)現(xiàn),當(dāng)時(shí)計(jì)劃抽時(shí)間寫一篇文章解析他的代碼實(shí)現(xiàn),今天突然看到作者自己寫了一篇文章,我感覺非常不錯(cuò),在這里分享給大家。 這篇文章是關(guān)于.NET開發(fā)者Victor Baybekov參加的一個(gè)名為"One Billion Row Challenge"的編程挑戰(zhàn),他使用.NET語言編寫了一個(gè)實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)的性能不僅打敗了Java,甚至超過了C++。 這個(gè)挑戰(zhàn)的目標(biāo)是處理一億行數(shù)據(jù),并提供對(duì)數(shù)據(jù)的快速查詢。原始版本只允許Java參與,但其他語言的開發(fā)者也希望參與其中,因此挑戰(zhàn)對(duì)其他語言開放。Victor Baybekov的實(shí)現(xiàn)不僅在特定的數(shù)據(jù)集上表現(xiàn)優(yōu)秀,而且在處理更通用的數(shù)據(jù)上也表現(xiàn)出色。他使用.NET的原因是,它的運(yùn)行速度快且易于使用。 文章中,Victor Baybekov詳細(xì)介紹了他的優(yōu)化過程,包括使用內(nèi)存映射文件,優(yōu)化哈希函數(shù),使用輸入規(guī)范,使用自定義字典,優(yōu)化內(nèi)部循環(huán)等。他還強(qiáng)調(diào)了.NET的速度和易用性,同時(shí)提到了.NET提供的不安全選項(xiàng),并不會(huì)使代碼自動(dòng)變得不安全。 對(duì)于.NET開發(fā)者來說,這篇文章提供了很多關(guān)于如何優(yōu)化代碼性能的實(shí)用信息。對(duì)于非.NET開發(fā)者來說,這篇文章也是一次了解.NET強(qiáng)大性能的好機(jī)會(huì)。 總的來說,這篇文章非常專業(yè),為.NET開發(fā)者提供了一種思路,即通過使用.NET的功能和優(yōu)化代碼,可以實(shí)現(xiàn)非常高的性能。同時(shí),這篇文章也證明了.NET在處理大量數(shù)據(jù)時(shí)的優(yōu)秀性能和易用性。 正文#在處理真實(shí)輸入數(shù)據(jù)時(shí),.NET平臺(tái)上的十億行挑戰(zhàn)比Java更快,甚至比C++還要快。 上周,GitHub上因?yàn)镚unnar Morling發(fā)起的“十億行挑戰(zhàn)”而熱鬧非凡。最初這是一個(gè)僅限Java參與的比賽,但后來其他語言的開發(fā)者也想加入這場(chǎng)樂趣。如果你不了解這個(gè)挑戰(zhàn)及其規(guī)則,請(qǐng)先閱讀這些鏈接。 https://github.com/gunnarmorling/1brc https://github.com/gunnarmorling/1brc/discussions/categories/show-and-tell 我也被這個(gè)挑戰(zhàn)深深吸引了。截至撰寫本文時(shí),我編寫的是目前最快的托管1BRC實(shí)現(xiàn)版本,它不僅在大家優(yōu)化的特定數(shù)據(jù)集上表現(xiàn)出色,而且在更通用的數(shù)據(jù)上也有很好的性能。更重要的是,我的結(jié)果在默認(rèn)數(shù)據(jù)上非常接近整體最優(yōu)的C++版本,并且在通用數(shù)據(jù)的情況下超過了它。 https://github.com/buybackoff/1brc 在下面的“結(jié)果”部分,我展示了不同語言和數(shù)據(jù)集的不同計(jì)時(shí)結(jié)果。在 “我的#1BRC之旅” 中,我展示了我的優(yōu)化歷程和性能時(shí)間線。然后我討論了為什么.NET在編寫這類代碼時(shí)既快速又易用。最后,我描述了我如何在日常工作中編寫高性能的.NET代碼,并邀請(qǐng)你如果對(duì)現(xiàn)代且快速的.NET感興趣,就來申請(qǐng)加入我們。 結(jié)果#除了我的代碼之外,我還在我的家庭實(shí)驗(yàn)室中專門搭建了一個(gè)基準(zhǔn)測(cè)試服務(wù)器。它擁有固定的CPU頻率并且能夠提供非常穩(wěn)定的結(jié)果。我投入了大量的精力來比較不同實(shí)現(xiàn)的性能。對(duì)于.NET和Java,我測(cè)量了同一代碼的JIT和AOT性能。 我沒有添加排名,因?yàn)榻Y(jié)果會(huì)根據(jù)數(shù)據(jù)的不同而有所不同。我用粗體突出顯示了按語言/JIT-AOT/數(shù)據(jù)集分組的最佳結(jié)果,并用黃色背景突出顯示了按數(shù)據(jù)集分組的整體最佳結(jié)果。 可能如預(yù)期的那樣,C++對(duì)于默認(rèn)數(shù)據(jù)集來說是最快的。然而,C++與.NET和Java之間的細(xì)微差別,即便是我也覺得有些出乎意料。我確實(shí)預(yù)料到了.NET會(huì)擊敗Java。這并非是第一次發(fā)生這種情況。在2016年,Aeron.NET客戶端1.0版本就比Java快,我當(dāng)時(shí)就在現(xiàn)場(chǎng)。 至于Rust,它很可能會(huì)成為總體的領(lǐng)導(dǎo)者。我們只需要等待直到實(shí)現(xiàn)是正確的。在撰寫本文時(shí),它還沒有做到。 最終,所有結(jié)果應(yīng)該會(huì)趨于某個(gè)物理極限和理想的CPU利用率。那么,一個(gè)有趣的問題將是,開發(fā)這樣的代碼付出了什么代價(jià)。對(duì)我來說,達(dá)到當(dāng)前這個(gè)點(diǎn)相當(dāng)容易,而且代碼非常簡(jiǎn)單。 擴(kuò)展數(shù)據(jù)集#默認(rèn)的數(shù)據(jù)生成器只有少量氣象站名稱,最大長(zhǎng)度低于AVX向量大小。這兩個(gè)屬性都有助于帶來極端的性能提升。然而,規(guī)格說明中提到,可能有多達(dá)1萬個(gè)獨(dú)特的氣象站,它們的名稱最多包含100個(gè)UTF8字節(jié)。
為了更公平的比較,我使用了兩個(gè)數(shù)據(jù)集: 原始的默認(rèn)數(shù)據(jù)集是用 擴(kuò)展的數(shù)據(jù)集包含了1萬個(gè)隨機(jī)的氣象站名稱,長(zhǎng)度可以達(dá)到規(guī)格所允許的最大值。它是用 在表格的底部,你可以看到一個(gè)單獨(dú)的部分,用于展示那些在默認(rèn)數(shù)據(jù)集上表現(xiàn)良好但無法正確處理1萬個(gè)數(shù)據(jù)的結(jié)果。這表明這些實(shí)現(xiàn)使用了超出規(guī)則說明的一些假設(shè),并且不公平地過度優(yōu)化了特定的情況。例如,最快的Rust版本的作者明確表示它不適用于1萬個(gè)數(shù)據(jù)。他更喜歡先編寫快速的代碼,然后再使其正確。 就我而言,我努力從一開始就編寫最通用的代碼。名稱可以是任意長(zhǎng)度,數(shù)字可以是任意非科學(xué)計(jì)數(shù)的浮點(diǎn)數(shù),行尾可以是 在Java再次變得更快之后(也是在短時(shí)間內(nèi)),我查看了規(guī)則,但沒有查看數(shù)據(jù)。對(duì)我來說,數(shù)字范圍的限制是最重要的,但氣象站名稱仍然可以是任意長(zhǎng)度。代碼會(huì)處理沖突,但對(duì)于真實(shí)世界輸入的氣象站名稱應(yīng)該很少發(fā)生沖突。不過我必須承認(rèn),有可能創(chuàng)建人為數(shù)據(jù),這些數(shù)據(jù)將會(huì)發(fā)生沖突,并將O(1)的哈希表查找變成O(N)的線性搜索。但即使在這種情況下,它仍然會(huì)工作,并且可能比參考實(shí)現(xiàn)還要快。也許我稍后會(huì)為了好玩而嘗試這樣做。 方法論#性能測(cè)試是在一個(gè)安靜的6C/12T Alder Lake CPU上進(jìn)行的,該CPU的頻率固定在2.5 GHz(關(guān)閉睿頻功能),搭配32GB DDR4 3200內(nèi)存,運(yùn)行Debian 12系統(tǒng),并且在Proxmox的特權(quán)LXC容器中進(jìn)行測(cè)試。由于基準(zhǔn)頻率是固定的,散熱狀況非常好(< 35°C),即使在持續(xù)100%負(fù)載下也不會(huì)發(fā)生降頻現(xiàn)象。 時(shí)間測(cè)量使用了 對(duì)于前兩名的.NET結(jié)果,我多次運(yùn)行了基準(zhǔn)測(cè)試,甚至為此重新啟動(dòng)了機(jī)器。確實(shí),在默認(rèn)數(shù)據(jù)上,根據(jù)JIT與AOT的不同,它們的排名有所不同。對(duì)于我的代碼來說,AOT略有不利,但對(duì)于Cameron Aavik的代碼來說,AOT顯著提高了性能。 我的#1BRC之旅#我咳嗽已經(jīng)超過2周了。新年期間咳得很厲害,以至于我在1月2日到3日請(qǐng)了假。1月3日,我喝著加了姜和蜂蜜的熱茶,刷著Twitter。我看到了Kevin Gosse關(guān)于這個(gè)挑戰(zhàn)的推文,我很喜歡這個(gè)想法。但我也清楚,這可能是一條通向深不見底的迷宮的入口,在那迷宮的底部,隱約能感受到曾經(jīng)浪費(fèi)時(shí)間的回憶。 然而,任務(wù)非常簡(jiǎn)單。我決定測(cè)量一下我寫一個(gè)非常簡(jiǎn)單但仍然快速的實(shí)現(xiàn)需要多長(zhǎng)時(shí)間。當(dāng)時(shí)是下午1:01,到下午3:17,我就完成了第一個(gè)版本,在我的測(cè)試機(jī)上處理默認(rèn)數(shù)據(jù)集/10K數(shù)據(jù)集分別需要13.5/18.0秒。然后,我開始瘋狂地優(yōu)化它。 通用版本,適用于任何輸入#起初,我甚至沒有嘗試針對(duì)規(guī)格進(jìn)行優(yōu)化。只是一個(gè)名稱和一個(gè)浮點(diǎn)數(shù),中間用分號(hào)隔開,每行一個(gè)測(cè)量值,在Linux上以 關(guān)鍵Idea#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/f1b81f8a590a8a42d5be8358e6ba30489e678592/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/82a17bc..f1b81f8?diff=split&w= 時(shí)間:13.490 / 17.991 (10K) 我的實(shí)現(xiàn)的關(guān)鍵思想直到最后都沒有改變。魔鬼隱藏在最微小的細(xì)節(jié)中。 內(nèi)存映射文件#使用mmap是顯而易見的,因?yàn)槲抑霸诟咝阅軋?chǎng)景下多次使用它,比如IPC環(huán)形緩沖區(qū)。它非常簡(jiǎn)單易用,所有復(fù)雜性都由操作系統(tǒng)管理。最近數(shù)據(jù)庫(kù)社區(qū)就是否使用mmap還是手動(dòng)內(nèi)存管理,即LMDB與其他方式之間進(jìn)行了激烈的討論。順便說一句,我是LMDB的大粉絲,甚至為其編寫了最快的.NET封裝。 盡管如此,為了避免munmap的慢速時(shí)間,我在這里嘗試了不使用mmap的方法。結(jié)果確實(shí)慢了一些,但并不太多。僅將文件復(fù)制到內(nèi)存中最多需要大約200毫秒的CPU帶寬,再加上不可避免的開銷,這就很能說明問題了。 Utf8Span#
public readonly unsafe struct Utf8Span : IEquatable<Utf8Span>{ private readonly byte* _pointer; private readonly int _len; // 構(gòu)造器 public ReadOnlySpan<byte> Span => new ReadOnlySpan<byte>(_pointer, _len); public bool Equals(Utf8Span other) => Span.SequenceEqual(other.Span); // 真是太懶了!連_hash中免費(fèi)可用的額外熵都沒用上。 // 但它在默認(rèn)數(shù)據(jù)集上運(yùn)行得相當(dāng)不錯(cuò)。 public override int GetHashCode() { // 使用前幾個(gè)字節(jié)作為哈希值 if (_len > 3) return *(int*)_pointer; if (_len > 1) return *(short*)_pointer; if (_len > 0) return *_pointer; return 0; } public override bool Equals(object? obj) => obj is Utf8Span other && Equals(other); public override string ToString() => new string((sbyte*)_pointer, 0, _len, Encoding.UTF8);} 為了高效地進(jìn)行哈希表查找,
平均值/最小值/最大值的高效更新#要計(jì)算運(yùn)行平均值,我們需要存儲(chǔ)總和和計(jì)數(shù)。這里沒有什么有趣的,我們都知道,自從編程幼兒園時(shí)代起,不是嗎? 更新 最小值/最大值 在數(shù)學(xué)上甚至更簡(jiǎn)單。只需檢查新值是否 小于/大于 之前的 最小值/最大值 ,并相應(yīng)地更新它們。然而,CPU不喜歡if語句,分支預(yù)測(cè)錯(cuò)誤的成本很高。然而,如果你再多想一點(diǎn)從統(tǒng)計(jì)學(xué)角度來看,對(duì)于任何穩(wěn)定的過程,實(shí)際上覆蓋 最小值/最大值 的機(jī)會(huì)隨著每一次觀測(cè)迅速下降。即使是股票價(jià)格,它們不是穩(wěn)定的,也不會(huì)每天、每月或每年都達(dá)到歷史新高。溫度據(jù)說“平均來說”是穩(wěn)定的,并且至少在幾個(gè)世紀(jì)的尺度上是穩(wěn)定的。 下面是一個(gè)簡(jiǎn)單的模擬,顯示了 最小值/最大值 分支所占比例的運(yùn)行情況。請(qǐng)注意,X軸是對(duì)數(shù)的。平均來說,僅在10次觀測(cè)后,兩個(gè)分支都不會(huì)被采用。 最小值/最大值分支概率#這個(gè)分析告訴我們使用分支而不是更重的無分支位運(yùn)算。我最終嘗試了無分支的選項(xiàng),但我有統(tǒng)計(jì)直覺,并且在第一個(gè)以及最終實(shí)現(xiàn)中都使用了if語句。無分支代碼使得執(zhí)行變得后端受限(如 perf stat 所見)。 public struct Summary{ // 注意,最初它們甚至不是浮點(diǎn)數(shù) public double Min; public double Max; public double Sum; public long Count; public double Average => Sum / Count; public void Apply(double value, bool isFirst) { // 第一個(gè)值總是會(huì)更新最小值/最大值 if (value < Min || isFirst) Min = value; if (value > Max || isFirst) Max = value; Sum += value; Count++; }} .NET 默認(rèn)字典#
還有一個(gè)非常好但不廣為人知的高性能工具類 通過取得摘要值的引用,我們避免了將其復(fù)制和更新到棧上/棧中,然后使用常規(guī) API 再?gòu)?fù)制回字典。記住, // 沒有結(jié)構(gòu)體復(fù)制 ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(result, nameUtf8Sp, out bool exists); // 對(duì)于類:分支、分配、代碼大小。謝謝,不用了。在 .NET 中,值類型規(guī)則。 // if (summary is null) summary = new Summary(); summary.Apply(value, !exists); // 這個(gè)方法在上面展示過 字節(jié)解析#對(duì)于解析字節(jié),我只是使用了 .NET 的 其他一切#性能僅取決于每個(gè)線程內(nèi)的 我很驚訝有些人準(zhǔn)備就僅僅使用 (P/)LINQ 的事實(shí)進(jìn)行爭(zhēng)論,僅僅因?yàn)樗麄兟犝f它很慢。他們顯然不夠了解 .NET,也沒有區(qū)分熱路徑和冷路徑。 var result = SplitIntoMemoryChunks() // 將整個(gè) mmap 分成每個(gè) CPU 相等的塊 .AsParallel().WithDegreeOfParallelism(_threads) // 分配到所有 CPU 核心 .Select((tuple => ProcessChunk(tuple.start, tuple.length))) // 在每個(gè) CPU 上執(zhí)行 ProcessChunk 工作。 .Aggregate((result, chunk) => { /* 合并結(jié)果 ... */ }) ; 優(yōu)化的浮點(diǎn)數(shù)解析#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/273def1abf9c9cc365b4309a3bd8d081a3eb7951/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/f1b81f8..273def1?diff=split&w= 時(shí)間:6.551 / 10.720 (10K) 在對(duì)代碼進(jìn)行性能分析后,我發(fā)現(xiàn) 我添加了一個(gè)通用的浮點(diǎn)數(shù)解析器,它有一個(gè)快速路徑,但在檢測(cè)到任何不規(guī)則情況時(shí)會(huì)回退到原始方法。所有的 這幾乎使性能翻了一番!還有一些其他的微優(yōu)化,只需點(diǎn)擊每個(gè)部分開頭的“與上一版本的差異”鏈接,即可查看所有更改。 [MethodImpl(MethodImplOptions.AggressiveInlining)]private double ParseNaive(ReadOnlySpan<byte> span){ double sign = 1; bool hasDot = false; ulong whole = 0; ulong fraction = 0; int fractionCount = 0; for (int i = 0; i < span.Length; i++) { var c = (int)span[i]; if (c == (byte)'-' && !hasDot && sign == 1 && whole == 0) { sign = -1; } else if (c == (byte)'.' && !hasDot) { hasDot = true; } else if ((uint)(c - '0') <= 9) { var digit = c - '0'; if (hasDot) { fractionCount++; fraction = fraction * 10 + (ulong)digit; } else { whole = whole * 10 + (ulong)digit; } } else { // 遇到任何不規(guī)則情況就回退到完整實(shí)現(xiàn) return double.Parse(span, NumberStyles.Float); } } return sign * (whole + fraction * _powersPtr[fractionCount]);} 優(yōu)化的哈希函數(shù)#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/e23c2bf8dace1450ad0411feaf54488795ec0fcb/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/273def1..e23c2bf?diff=split&w= 時(shí)間:6.313 / 10.384 (10K) 它不再像初始版本那樣懶惰,它包含了長(zhǎng)度和最初的幾個(gè)字節(jié)的組合。免費(fèi)獲得了超過3%的收益。 如果哈??偸橇悖覀兪褂镁€性搜索,有一些評(píng)論和最壞情況下的測(cè)量。 public override int GetHashCode(){ // 這里我們使用前4個(gè)字符(如果是ASCII)和長(zhǎng)度來計(jì)算哈希。 // 最壞的情況是前綴,如 Port/Saint 且長(zhǎng)度相同, // 這對(duì)于人類地理名稱來說相當(dāng)罕見。 // .NET 字典顯然會(huì)因?yàn)闆_突而變慢,但仍然可以工作。 // 如果我們只保留 `*_pointer`,運(yùn)行時(shí)間仍然合理,大約9秒。 // 僅使用 `if (_len > 0) return (_len * 820243) ^ (*_pointer);` 耗時(shí)5.8秒。 // 僅返回0 - 最糟糕的哈希函數(shù)和線性搜索 - 運(yùn)行時(shí)間慢了12倍,為56秒。 // 魔術(shù)數(shù)字820243是包含2024的最大快樂素?cái)?shù),來自 https://prime-numbers.info/list/happy-primes-page-9 if (_len > 3) return (_len * 820243) ^ (*(int*)_pointer); // 只添加了 ^ 之前的部分 if (_len > 1) return *(short*)_pointer; if (_len > 0) return *_pointer; return 0;} 在這個(gè)改變之后,我開始研究哪些規(guī)則可能對(duì)性能有用。 使用輸入規(guī)則#挑戰(zhàn)的規(guī)則說明名字總是少于100個(gè)UTF8字節(jié),最多有10K個(gè)獨(dú)特的名字,溫度在-99.9到99.9之間( 我認(rèn)為針對(duì)規(guī)則進(jìn)行優(yōu)化是完全可以接受的??赡苡姓嬲臍庀笳井a(chǎn)生這樣的數(shù)據(jù),而代碼在我出生前就已經(jīng)寫好了。然而,我不喜歡人們開始針對(duì)特定的數(shù)據(jù)集/生成器進(jìn)行優(yōu)化。因此,在這次比較中,我沒有接受那些不能處理10K數(shù)據(jù)集的實(shí)現(xiàn)。即使使用規(guī)格,我的代碼也支持任何名字長(zhǎng)度。 將數(shù)字解析為整數(shù)#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/e5d34c92a82a446d876089a1e1872da54bf64ebb/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/e23c2bf..e5d34c9?diff=split&w= 時(shí)間:5.229 / 8.627 (10K) 僅僅利用溫度在-99.9到99.9之間的事實(shí)。我們只有4種情況,可以為此進(jìn)行優(yōu)化: ...;-99.9 ...;-9.9 ...;9.9 ...;99.9 設(shè)置字典容量#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/3644b251cda38abd620bda644efda12951020042/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/e5d34c9..3644b25?diff=split&w=# 時(shí)間:4.341 / 8.951 (10K) 這真是太傻了!但在我迫切需要提升性能的時(shí)候,這就像罐頭食品一樣珍貴。僅僅一行代碼/改動(dòng)五個(gè)字符就能獲得17%的性能提升。 優(yōu)化的IndexOf#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/7fdd17a755665910ecfabb4667b5bda277531e39/1brc 時(shí)間:4.040 / 8.609 (10K) 在剩余部分小于32字節(jié)時(shí),手動(dòng)AVX2在Span中搜索字節(jié),并回退到 // 在 Utf8Span 內(nèi)部 [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int IndexOf(int start, byte needle) { if (Avx2.IsSupported) { var needleVec = new Vector<byte>(needle); Vector<byte> vec; while (true) { if (start + Vector<byte>.Count >= Length) goto FALLBACK; var data = Unsafe.ReadUnaligned<Vector<byte>>(Pointer + start); vec = Vector.Equals(data, needleVec); if (!vec.Equals(Vector<byte>.Zero)) break; start += Vector<byte>.Count; } var matches = vec.AsVector256(); var mask = Avx2.MoveMask(matches); int tzc = BitOperations.TrailingZeroCount((uint)mask); return start + tzc; } FALLBACK: int indexOf = SliceUnsafe(start).Span.IndexOf(needle); return indexOf < 0 ? Length : start + indexOf; } 積極的使用本機(jī)整數(shù)#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/d6c8e48b07821a05a1582f0e98f949360e3b4bd9/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/7fdd17a..d6c8e48?diff=split&w= 時(shí)間:3.693 / 8.604 (10K) 在本機(jī)環(huán)境中,使用size_t本機(jī)大小類型作為偏移和長(zhǎng)度是正常的,因?yàn)镃PU處理本機(jī)字更快。在.NET中,大多數(shù)公共API接受32位int。CPU必須每次將其擴(kuò)展為nint。但內(nèi)部.NET本身使用本機(jī)整數(shù)。 例如,這是帶有注釋的 // 優(yōu)化的基于字節(jié)的SequenceEquals。這個(gè)的“l(fā)ength”參數(shù)被聲明為nuint而不是int, // 因?yàn)槲覀円灿盟鼇硖幚沓齜yte以外的類型,其中長(zhǎng)度一旦通過sizeof(T)縮放就會(huì)超過2Gb。 [Intrinsic] // 對(duì)常量長(zhǎng)度展開 public static unsafe bool SequenceEqual(ref byte first, ref byte second, nuint length) { bool result; // 使用nint進(jìn)行算術(shù)運(yùn)算以避免不必要的64->32->64截?cái)?/p> if (length >= (nuint)sizeof(nuint)) 使用自定義字典#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/8841e83e2abfb5f57a872cbea4c979c9b9e49178/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/d6c8e48..8841e83?diff=split&w= 時(shí)間:3.272 / 8.232 (10K) 直到這一點(diǎn),我仍然使用的是默認(rèn)的.NET字典。但由于規(guī)格說明最多有10K個(gè)獨(dú)特的名字,我可以利用這個(gè)規(guī)則。 詳細(xì)信息以后再補(bǔ)充。 快速 Utf8Span.Equals#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/9ed39221ec7db8f89e8e2a0702d43a184cc5e879/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/8841e83..9ed3922?diff=split&w= 時(shí)間:2.773 / 6.635 (10K) 我花了一些努力嘗試擊敗 為了確保安全,我確保最后一個(gè)大塊不是在文件末尾結(jié)束,而是至少在距離末尾 優(yōu)化內(nèi)循環(huán)#提交時(shí)的文件: https://github.com/buybackoff/1brc/tree/1051e06052d5a8a95fa0aee461e37d969532aa65/1brc 與上一版本的差異: https://github.com/buybackoff/1brc/compare/9ed3922..1051e06?diff=split&w= 時(shí)間:2.204 / 4.811 (10K)
詳細(xì)信息待定 性能時(shí)間線#以下是討論上述每次更改后性能演變的時(shí)間線。 .NET 非???a class="esa-anchor" style="margin: 0px 0px 0px 8px; padding: 0px; text-decoration-line: none; opacity: 0; transition: opacity 0.3s ease 0s;">#.NET 非常快。而且每個(gè)新版本都在變得更快。有些人開玩笑說,對(duì)于 .NET 的最佳性能優(yōu)化就是更新它 - 對(duì)于大多數(shù)用戶來說,這可能是真的。 每次發(fā)布新版本時(shí),.NET 團(tuán)隊(duì)的 Stephen Toub 都會(huì)發(fā)表一篇巨大的博客文章,介紹自上次發(fā)布以來的每一個(gè)微小性能改進(jìn)。這些文章的龐大體量表明,他們非常關(guān)心性能的提升。 不安全代碼#.NET 允許你直接使用指針。這使得它類似于 C 語言。如果內(nèi)循環(huán)受 CPU 限制,所有數(shù)組都可以被固定并在沒有邊界檢查的情況下訪問,或者我們可以直接像在這個(gè) 1BRC 案例中那樣直接處理本地內(nèi)存。 另外,.NET 提供了一個(gè)較新的 Unsafe 類,它本質(zhì)上與舊的 unsafe 關(guān)鍵字 + 指針做同樣的事情,但使用托管引用。這允許跳過固定數(shù)組,同時(shí)仍然是 GC 安全的。 不安全選項(xiàng)的存在并不會(huì)自動(dòng)使代碼不安全。有“不安全的 Unsafe”和“安全的 Unsafe”。例如,如果我們確保數(shù)組邊界,但不能使 JIT 省略邊界檢查(如在自定義字典案例和 易用的向量化函數(shù)#.NET 有非常容易使用的 SIMD 內(nèi)在函數(shù)。我們可以直接使用 SSE2/AVX2/BMI API,或者使用跨平臺(tái)跨架構(gòu)的 .NET 的范圍#.NET 不強(qiáng)迫我們每次都編寫低級(jí)的不安全 SIMD 代碼。當(dāng)性能不重要時(shí),我們可以只使用 LINQ。這很好。即使在這個(gè) 1BRC 挑戰(zhàn)中也是如此。真的。 C# 與 F##F# 在默認(rèn)數(shù)據(jù)集和10K數(shù)據(jù)集上都展現(xiàn)出了不俗的性能。我與 F# 的關(guān)系頗為復(fù)雜。博客上的一篇長(zhǎng)篇文章講述了我為何放棄 F# 轉(zhuǎn)而選擇 C# 的原因。主要是因?yàn)樾阅軉栴}(包括生成的代碼和工具的性能),盡管我喜歡 F# 的語法和社區(qū)。 然而,F(xiàn)# 的速度之快并不讓我感到驚訝。它一直在穩(wěn)步提升,或許有一天我會(huì)再次使用 F#. 例如,可恢復(fù)代碼和可恢復(fù)狀態(tài)機(jī)是我一直在關(guān)注的非常強(qiáng)大的功能。.NET 原生支持的 在這里,我不得不提到,我也通過一系列在2020年的提交,大幅提高了 F# 性能,使其核心的 當(dāng)然,正如作者所承認(rèn)的,F(xiàn)rank Krueger 的 F# 實(shí)現(xiàn)遠(yuǎn)非典型的函數(shù)式 F# 代碼。但是,如果你已經(jīng)在使用 F# 代碼,而且不想碰 C#,你也可以在 F# 中寫類似 C 的代碼。只是不要過度,把它隱藏在純函數(shù)里,然后對(duì)外保密。 😉 高性能 .NET 代碼作為日常工作#在 ABC Arbitrage,我有機(jī)會(huì)每天都處理性能關(guān)鍵的 .NET 代碼。公司多年前從 C++ 遷移到 .NET,這在可維護(hù)性、靈活性、性能和成本方面都是巨大的成功。像1BRC中看到的優(yōu)化在我們的代碼中并不少見。當(dāng)然,并非我們所有的代碼都是那樣的。我們還有很多易讀的現(xiàn)代 C# 代碼,甚至 LINQ 也不是禁止的,除非它在交易路徑上。我們總是跟上最新的 .NET 發(fā)展,通常在新的主要版本發(fā)布后幾周內(nèi)就會(huì)在生產(chǎn)環(huán)境中使用(例如,我們已經(jīng)“長(zhǎng)時(shí)間”使用 .NET 8 了)。 我們?cè)?ABC 使用并貢獻(xiàn)了許多開源項(xiàng)目,并且我們也維護(hù)一些。由 Olivier Coanet 維護(hù)的著名高性能線程間消息傳遞庫(kù) Disruptor-net 的 .NET 移植版本是我們交易平臺(tái)的核心,處理著每一個(gè)市場(chǎng)行情和每一個(gè)交易訂單。我貢獻(xiàn)了一些類似上文討論的微優(yōu)化。由 Lucas Trzesniewski 作為主要貢獻(xiàn)者的輕量級(jí)點(diǎn)對(duì)點(diǎn)服務(wù)總線 Zebus,自2013年以來一直在 ABC Arbitrage 的生產(chǎn)環(huán)境中運(yùn)行,每天處理數(shù)億條消息,并協(xié)調(diào)整個(gè)基礎(chǔ)設(shè)施。由 Lucas、Romain Verdier 和其他人貢獻(xiàn)的日志庫(kù) ZeroLog,速度之快且零分配,以至于我們甚至可以在最延遲敏感的路徑上輕松使用它。公司倉(cāng)庫(kù)中還有其他項(xiàng)目,以及我們現(xiàn)任和前任同事的許多其他貢獻(xiàn)。我們真正擁抱開源 😍 如果你喜歡使用現(xiàn)代 .NET 編寫高性能代碼,并且想在巴黎享受一點(diǎn)樂趣,為何不加入我們呢?我們有幾個(gè)開放的 .NET 職位。如果你感到?jīng)_動(dòng),就在 dotnet📧abc-arbitrage.com 向我們發(fā)送申請(qǐng)吧。
原文信息#原文鏈接:https://hotforknowledge.com/2024/01/13/7-1brc-in-dotnet-even-faster-than-java-cpp/ 轉(zhuǎn)自博客園https://www.cnblogs.com/InCerry/p/17964592/7-1brc-in-dotnet-even-faster-than-java-cpp 該文章在 2024/1/29 10:43:36 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |