狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

如何使用.NET在2.2秒內處理10億行數據(1brc挑戰)

freeflydom
2024年1月29日 10:7 本文熱度 740

譯者注#

在上周我就關注到了在github上有1brc這樣一個挑戰,當時看到了由Victor Baybekov提交了.NET下最快的實現,當時計劃抽時間寫一篇文章解析他的代碼實現,今天突然看到作者自己寫了一篇文章,我感覺非常不錯,在這里分享給大家。

這篇文章是關于.NET開發者Victor Baybekov參加的一個名為"One Billion Row Challenge"的編程挑戰,他使用.NET語言編寫了一個實現,這個實現的性能不僅打敗了Java,甚至超過了C++。

這個挑戰的目標是處理一億行數據,并提供對數據的快速查詢。原始版本只允許Java參與,但其他語言的開發者也希望參與其中,因此挑戰對其他語言開放。Victor Baybekov的實現不僅在特定的數據集上表現優秀,而且在處理更通用的數據上也表現出色。他使用.NET的原因是,它的運行速度快且易于使用。

文章中,Victor Baybekov詳細介紹了他的優化過程,包括使用內存映射文件,優化哈希函數,使用輸入規范,使用自定義字典,優化內部循環等。他還強調了.NET的速度和易用性,同時提到了.NET提供的不安全選項,并不會使代碼自動變得不安全。

對于.NET開發者來說,這篇文章提供了很多關于如何優化代碼性能的實用信息。對于非.NET開發者來說,這篇文章也是一次了解.NET強大性能的好機會。

總的來說,這篇文章非常專業,為.NET開發者提供了一種思路,即通過使用.NET的功能和優化代碼,可以實現非常高的性能。同時,這篇文章也證明了.NET在處理大量數據時的優秀性能和易用性。

正文#

在處理真實輸入數據時,.NET平臺上的十億行挑戰比Java更快,甚至比C++還要快。

上周,GitHub上因為Gunnar Morling發起的“十億行挑戰”而熱鬧非凡。最初這是一個僅限Java參與的比賽,但后來其他語言的開發者也想加入這場樂趣。如果你不了解這個挑戰及其規則,請先閱讀這些鏈接。

https://github.com/gunnarmorling/1brc

https://github.com/gunnarmorling/1brc/discussions/categories/show-and-tell

我也被這個挑戰深深吸引了。截至撰寫本文時,我編寫的是目前最快的托管1BRC實現版本,它不僅在大家優化的特定數據集上表現出色,而且在更通用的數據上也有很好的性能。更重要的是,我的結果在默認數據上非常接近整體最優的C++版本,并且在通用數據的情況下超過了它。

https://github.com/buybackoff/1brc

在下面的“結果”部分,我展示了不同語言和數據集的不同計時結果。在 “我的#1BRC之旅” 中,我展示了我的優化歷程和性能時間線。然后我討論了為什么.NET在編寫這類代碼時既快速又易用。最后,我描述了我如何在日常工作中編寫高性能的.NET代碼,并邀請你如果對現代且快速的.NET感興趣,就來申請加入我們。

結果#

除了我的代碼之外,我還在我的家庭實驗室中專門搭建了一個基準測試服務器。它擁有固定的CPU頻率并且能夠提供非常穩定的結果。我投入了大量的精力來比較不同實現的性能。對于.NET和Java,我測量了同一代碼的JIT和AOT性能。

我沒有添加排名,因為結果會根據數據的不同而有所不同。我用粗體突出顯示了按語言/JIT-AOT/數據集分組的最佳結果,并用黃色背景突出顯示了按數據集分組的整體最佳結果。

https://hotforknowledge.com/2024/01/13/7-1brc-in-dotnet-even-faster-than-java-cpp/results_details.png

可能如預期的那樣,C++對于默認數據集來說是最快的。然而,C++與.NET和Java之間的細微差別,即便是我也覺得有些出乎意料。我確實預料到了.NET會擊敗Java。這并非是第一次發生這種情況。在2016年,Aeron.NET客戶端1.0版本就比Java快,我當時就在現場。

至于Rust,它很可能會成為總體的領導者。我們只需要等待直到實現是正確的。在撰寫本文時,它還沒有做到。

最終,所有結果應該會趨于某個物理極限和理想的CPU利用率。那么,一個有趣的問題將是,開發這樣的代碼付出了什么代價。對我來說,達到當前這個點相當容易,而且代碼非常簡單。

擴展數據集#

默認的數據生成器只有少量氣象站名稱,最大長度低于AVX向量大小。這兩個屬性都有助于帶來極端的性能提升。然而,規格說明中提到,可能有多達1萬個獨特的氣象站,它們的名稱最多包含100個UTF8字節。

“我鼓勵參賽者嘗試一下,并將其作為優化的目標。能夠看到自己位于排行榜頂端無疑是令人興奮的,但設計一個能夠適應超出416個最大長度為26個字符的車站名稱的解決方案更有趣(也更有用!)”

以上是Marko Topolnik的話,他最近提交了一個更通用的生成器。

為了更公平的比較,我使用了兩個數據集:

原始的默認數據集是用create_measurements.sh生成的。它的大小超過12GB。這個數據集只有416個氣象站名稱,最大長度為26個字符。

擴展的數據集包含了1萬個隨機的氣象站名稱,長度可以達到規格所允許的最大值。它是用create_measurements3.sh生成的,大小超過17GB。詳情見上面的引用鏈接。

在表格的底部,你可以看到一個單獨的部分,用于展示那些在默認數據集上表現良好但無法正確處理1萬個數據的結果。這表明這些實現使用了超出規則說明的一些假設,并且不公平地過度優化了特定的情況。例如,最快的Rust版本的作者明確表示它不適用于1萬個數據。他更喜歡先編寫快速的代碼,然后再使其正確。

就我而言,我努力從一開始就編寫最通用的代碼。名稱可以是任意長度,數字可以是任意非科學計數的浮點數,行尾可以是\r\n。就在一周前,我甚至還能用這樣的代碼超越頂級Java結果。

在Java再次變得更快之后(也是在短時間內),我查看了規則,但沒有查看數據。對我來說,數字范圍的限制是最重要的,但氣象站名稱仍然可以是任意長度。代碼會處理沖突,但對于真實世界輸入的氣象站名稱應該很少發生沖突。不過我必須承認,有可能創建人為數據,這些數據將會發生沖突,并將O(1)的哈希表查找變成O(N)的線性搜索。但即使在這種情況下,它仍然會工作,并且可能比參考實現還要快。也許我稍后會為了好玩而嘗試這樣做。

方法論#

性能測試是在一個安靜的6C/12T Alder Lake CPU上進行的,該CPU的頻率固定在2.5 GHz(關閉睿頻功能),搭配32GB DDR4 3200內存,運行Debian 12系統,并且在Proxmox的特權LXC容器中進行測試。由于基準頻率是固定的,散熱狀況非常好(< 35°C),即使在持續100%負載下也不會發生降頻現象。

時間測量使用了hyperfine --warmup 1 --runs 5 './1brc path/to/file'命令。由于系統中沒有噪聲,結果非常穩定。更多細節請查看結果表下方的鏈接。

對于前兩名的.NET結果,我多次運行了基準測試,甚至為此重新啟動了機器。確實,在默認數據上,根據JIT與AOT的不同,它們的排名有所不同。對于我的代碼來說,AOT略有不利,但對于Cameron Aavik的代碼來說,AOT顯著提高了性能。

我的#1BRC之旅#

我咳嗽已經超過2周了。新年期間咳得很厲害,以至于我在1月2日到3日請了假。1月3日,我喝著加了姜和蜂蜜的熱茶,刷著Twitter。我看到了Kevin Gosse關于這個挑戰的推文,我很喜歡這個想法。但我也清楚,這可能是一條通向深不見底的迷宮的入口,在那迷宮的底部,隱約能感受到曾經浪費時間的回憶。

然而,任務非常簡單。我決定測量一下我寫一個非常簡單但仍然快速的實現需要多長時間。當時是下午1:01,到下午3:17,我就完成了第一個版本,在我的測試機上處理默認數據集/10K數據集分別需要13.5/18.0秒。然后,我開始瘋狂地優化它。

通用版本,適用于任何輸入#

起初,我甚至沒有嘗試針對規格進行優化。只是一個名稱和一個浮點數,中間用分號隔開,每行一個測量值,在Linux上以\n結束,或在Windows上以\r\n結束。重復1B次。

關鍵Idea#

提交時的文件: https://github.com/buybackoff/1brc/tree/f1b81f8a590a8a42d5be8358e6ba30489e678592/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/82a17bc..f1b81f8?diff=split&w=

時間:13.490 / 17.991 (10K)

我的實現的關鍵思想直到最后都沒有改變。魔鬼隱藏在最微小的細節中。

內存映射文件#

使用mmap是顯而易見的,因為我之前在高性能場景下多次使用它,比如IPC環形緩沖區。它非常簡單易用,所有復雜性都由操作系統管理。最近數據庫社區就是否使用mmap還是手動內存管理,即LMDB與其他方式之間進行了激烈的討論。順便說一句,我是LMDB的大粉絲,甚至為其編寫了最快的.NET封裝

盡管如此,為了避免munmap的慢速時間,我在這里嘗試了不使用mmap的方法。結果確實慢了一些,但并不太多。僅將文件復制到內存中最多需要大約200毫秒的CPU帶寬,再加上不可避免的開銷,這就很能說明問題了。

Utf8Span#

Utf8Span可能是實現高性能的最重要思想。它是一個結構體,存儲了映射文件中UTF8段的指針和長度。數據從未被復制,即使當span作為字典中的鍵使用時也是如此。它從未從UTF8轉換成UTF16,直到最后在排序和打印最終結果時才轉換。

public readonly unsafe struct Utf8Span : IEquatable<Utf8Span>{
    private readonly byte* _pointer;
    private readonly int _len;
    // 構造器
    public ReadOnlySpan<byte> Span => new ReadOnlySpan<byte>(_pointer, _len);
    public bool Equals(Utf8Span other) => Span.SequenceEqual(other.Span);
    // 真是太懶了!連_hash中免費可用的額外熵都沒用上。
    // 但它在默認數據集上運行得相當不錯。
    public override int GetHashCode()
    {
        // 使用前幾個字節作為哈希值
        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);}

為了高效地進行哈希表查找,Equals 和 GetHashCode 成為最重要的方法。

Span.SequenceEqual()API 通常難以超越,但該調用不會內聯,對于小數據來說太重了。后來我找到了一種簡單的加速方法,但這需要對分塊以及 Equals 本身進行更改。

平均值/最小值/最大值的高效更新#

要計算運行平均值,我們需要存儲總和和計數。這里沒有什么有趣的,我們都知道,自從編程幼兒園時代起,不是嗎?

更新 最小值/最大值 在數學上甚至更簡單。只需檢查新值是否 小于/大于 之前的 最小值/最大值 ,并相應地更新它們。然而,CPU不喜歡if語句,分支預測錯誤的成本很高。然而,如果你再多想一點從統計學角度來看,對于任何穩定的過程,實際上覆蓋 最小值/最大值 的機會隨著每一次觀測迅速下降。即使是股票價格,它們不是穩定的,也不會每天、每月或每年都達到歷史新高。溫度據說“平均來說”是穩定的,并且至少在幾個世紀的尺度上是穩定的。

下面是一個簡單的模擬,顯示了 最小值/最大值 分支所占比例的運行情況。請注意,X軸是對數的。平均來說,僅在10次觀測后,兩個分支都不會被采用。

最小值/最大值分支概率#

這個分析告訴我們使用分支而不是更重的無分支位運算。我最終嘗試了無分支的選項,但我有統計直覺,并且在第一個以及最終實現中都使用了if語句。無分支代碼使得執行變得后端受限(如 perf stat 所見)。

public struct Summary{
    // 注意,最初它們甚至不是浮點數
    public double Min;
    public double Max;
    public double Sum;
    public long Count;
    public double Average => Sum / Count;
    public void Apply(double value, bool isFirst)
    {
        // 第一個值總是會更新最小值/最大值
        if (value < Min || isFirst)
            Min = value;
        if (value > Max || isFirst)
            Max = value;
        Sum += value;
        Count++;
    }}

.NET 默認字典#

Dictionary<TKey,TValue> 幾乎總是足夠好的選擇,也不是首先需要擔心的事情。在我的案例中,它是 Dictionary<Utf8Span,Summary>。.NET 的 JIT(即時編譯器)在沒有我做任何額外努力的情況下,內聯了對 Utf8Span 的 Equals 和 GetHashCode 方法的調用。

還有一個非常好但不廣為人知的高性能工具類 CollectionsMarshal,用于通過引用訪問字典值。其方法 GetValueRefOrAddDefault 對于更新摘要數據特別有幫助。

通過取得摘要值的引用,我們避免了將其復制和更新到棧上/棧中,然后使用常規 API 再復制回字典。記住,Summary 是一個可變的結構體,對其引用調用方法不會導致復制。同時想象一下,如果 Summary 是一個類,那么即使使用相同的 GetValueRefOrAddDefault,人們也必須檢查空值并創建新實例的不必要開銷。一個默認的結構體無需額外步驟即可準備存儲數據。

// 沒有結構體復制

ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(result, nameUtf8Sp, out bool exists);

// 對于類:分支、分配、代碼大小。謝謝,不用了。在 .NET 中,值類型規則。

// if (summary is null) summary = new Summary(); 

summary.Apply(value, !exists); // 這個方法在上面展示過

字節解析#

對于解析字節,我只是使用了 .NET 的 Span.IndexOf 和 double.Parse() API。

其他一切#

性能僅取決于每個線程內的 ProcessChunk。對于其他一切,我們可以編寫任何懶惰或簡單的代碼。例如,我喜歡使用 LINQ/PLINQ 管道,尤其是當我能夠創建一個長的和懶惰的計算時。但我可以很容易地用一個 for 循環打破這樣的管道,而不需要多想,因為這對性能或可讀性都無關緊要。例如,在實際的第一次提交中,聚合是在循環中進行的,僅僅因為這樣想起來更簡單,但完成后它被復制粘貼到了 .Aggregate() 方法中。

我很驚訝有些人準備就僅僅使用 (P/)LINQ 的事實進行爭論,僅僅因為他們聽說它很慢。他們顯然不夠了解 .NET,也沒有區分熱路徑和冷路徑。

var result = SplitIntoMemoryChunks() // 將整個 mmap 分成每個 CPU 相等的塊
    .AsParallel().WithDegreeOfParallelism(_threads) // 分配到所有 CPU 核心
    .Select((tuple => ProcessChunk(tuple.start, tuple.length))) // 在每個 CPU 上執行 ProcessChunk 工作。
    .Aggregate((result, chunk) => { /* 合并結果 ... */ })
    ;

優化的浮點數解析#

提交時的文件: https://github.com/buybackoff/1brc/tree/273def1abf9c9cc365b4309a3bd8d081a3eb7951/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/f1b81f8..273def1?diff=split&w=

時間:6.551 / 10.720 (10K)

在對代碼進行性能分析后,我發現 double.Parse() 占用了大約57%的運行時間。字典查找占了大約24%。

我添加了一個通用的浮點數解析器,它有一個快速路徑,但在檢測到任何不規則情況時會回退到原始方法。所有的 [-]?[0-9]+[.][0-9]+ 浮點數都會走這個實現的快速路徑。

這幾乎使性能翻了一番!還有一些其他的微優化,只需點擊每個部分開頭的“與上一版本的差異”鏈接,即可查看所有更改。

[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
        {
            // 遇到任何不規則情況就回退到完整實現
            return double.Parse(span, NumberStyles.Float);
        }
    }
    return sign * (whole + fraction * _powersPtr[fractionCount]);}

優化的哈希函數#

提交時的文件: https://github.com/buybackoff/1brc/tree/e23c2bf8dace1450ad0411feaf54488795ec0fcb/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/273def1..e23c2bf?diff=split&w=

時間:6.313 / 10.384 (10K)

它不再像初始版本那樣懶惰,它包含了長度和最初的幾個字節的組合。免費獲得了超過3%的收益。

如果哈希總是零,我們使用線性搜索,有一些評論和最壞情況下的測量。

public override int GetHashCode(){
    // 這里我們使用前4個字符(如果是ASCII)和長度來計算哈希。
    // 最壞的情況是前綴,如 Port/Saint 且長度相同,
    // 這對于人類地理名稱來說相當罕見。
    // .NET 字典顯然會因為沖突而變慢,但仍然可以工作。
    // 如果我們只保留 `*_pointer`,運行時間仍然合理,大約9秒。
    // 僅使用 `if (_len > 0) return (_len * 820243) ^ (*_pointer);` 耗時5.8秒。
    // 僅返回0 - 最糟糕的哈希函數和線性搜索 - 運行時間慢了12倍,為56秒。
    // 魔術數字820243是包含2024的最大快樂素數,來自 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;}

在這個改變之后,我開始研究哪些規則可能對性能有用。

使用輸入規則#

挑戰的規則說明名字總是少于100個UTF8字節,最多有10K個獨特的名字,溫度在-99.9到99.9之間([-]?[0-9]?[0-9][.][0-9]),行總是以\n結束。

我認為針對規則進行優化是完全可以接受的。可能有真正的氣象站產生這樣的數據,而代碼在我出生前就已經寫好了。然而,我不喜歡人們開始針對特定的數據集/生成器進行優化。因此,在這次比較中,我沒有接受那些不能處理10K數據集的實現。即使使用規格,我的代碼也支持任何名字長度。

將數字解析為整數#

提交時的文件: https://github.com/buybackoff/1brc/tree/e5d34c92a82a446d876089a1e1872da54bf64ebb/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/e23c2bf..e5d34c9?diff=split&w=

時間:5.229 / 8.627 (10K)

僅僅利用溫度在-99.9到99.9之間的事實。我們只有4種情況,可以為此進行優化:

...;-99.9
...;-9.9
...;9.9
...;99.9

設置字典容量#

提交時的文件: https://github.com/buybackoff/1brc/tree/3644b251cda38abd620bda644efda12951020042/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/e5d34c9..3644b25?diff=split&w=#

時間:4.341 / 8.951 (10K)

這真是太傻了!但在我迫切需要提升性能的時候,這就像罐頭食品一樣珍貴。僅僅一行代碼/改動五個字符就能獲得17%的性能提升。

優化的IndexOf#

提交時的文件: https://github.com/buybackoff/1brc/tree/7fdd17a755665910ecfabb4667b5bda277531e39/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/3644b25..7fdd17a?diff=split&w=#diff-50d5d1069929df17bbf6f330e04035cfaafa17de2e48ab86ce2dbd0de338528aR99-R125

時間:4.040 / 8.609 (10K)

在剩余部分小于32字節時,手動AVX2在Span中搜索字節,并回退到Span.IndexOf

// 在 Utf8Span 內部

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

}

積極的使用本機整數#

提交時的文件: https://github.com/buybackoff/1brc/tree/d6c8e48b07821a05a1582f0e98f949360e3b4bd9/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/7fdd17a..d6c8e48?diff=split&w=

時間:3.693 / 8.604 (10K)

在本機環境中,使用size_t本機大小類型作為偏移和長度是正常的,因為CPU處理本機字更快。在.NET中,大多數公共API接受32位int。CPU必須每次將其擴展為nint。但內部.NET本身使用本機整數。

例如,這是帶有注釋的SpanHelpers.SequenceEqual代碼:

// 優化的基于字節的SequenceEquals。這個的“length”參數被聲明為nuint而不是int,

// 因為我們也用它來處理除byte以外的類型,其中長度一旦通過sizeof(T)縮放就會超過2Gb。

[Intrinsic] // 對常量長度展開

public static unsafe bool SequenceEqual(ref byte first, ref byte second, nuint length)

{

    bool result;

    // 使用nint進行算術運算以避免不必要的64->32->64截斷

    if (length >= (nuint)sizeof(nuint))

使用自定義字典#

提交時的文件: https://github.com/buybackoff/1brc/tree/8841e83e2abfb5f57a872cbea4c979c9b9e49178/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/d6c8e48..8841e83?diff=split&w=

時間:3.272 / 8.232 (10K)

直到這一點,我仍然使用的是默認的.NET字典。但由于規格說明最多有10K個獨特的名字,我可以利用這個規則。

詳細信息以后再補充。

快速 Utf8Span.Equals#

提交時的文件: https://github.com/buybackoff/1brc/tree/9ed39221ec7db8f89e8e2a0702d43a184cc5e879/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/8841e83..9ed3922?diff=split&w=

時間:2.773 / 6.635 (10K)

我花了一些努力嘗試擊敗 Span.SequenceEqual 在小尺寸上的性能。嘗試復制實現的部分并內聯它,但沒有任何效果。然后我有了一個瘋狂的想法,允許代碼讀取超出 Utf8Span.Length 的內容。然后我可以只使用一個 AVX2 向量,將長度之后的字節設置為零,并比較向量。這將是完全不安全的,并且會導致段錯誤,但只是在十億個觀測值中的最后一個單獨觀測值中。

為了確保安全,我確保最后一個大塊不是在文件末尾結束,而是至少在距離末尾4 x Vector256<byte>.Count的新行開始處結束。我將剩余部分復制到一個比數據大得多的內存緩沖區中,這是安全使用的。

優化內循環#

提交時的文件: https://github.com/buybackoff/1brc/tree/1051e06052d5a8a95fa0aee461e37d969532aa65/1brc

與上一版本的差異: https://github.com/buybackoff/1brc/compare/9ed3922..1051e06?diff=split&w=

時間:2.204 / 4.811 (10K)

  • 更快的整數解析結合新行索引計算;

  • 更快的 IndexOf,也依賴于讀取超出 Utf8Span.Length 的內容;

  • 更快的 ProcessChunk 循環。

詳細信息待定

性能時間線#

以下是討論上述每次更改后性能演變的時間線。

.NET 非常快#

.NET 非常快。而且每個新版本都在變得更快。有些人開玩笑說,對于 .NET 的最佳性能優化就是更新它 - 對于大多數用戶來說,這可能是真的。

每次發布新版本時,.NET 團隊的 Stephen Toub 都會發表一篇巨大的博客文章,介紹自上次發布以來的每一個微小性能改進。這些文章的龐大體量表明,他們非常關心性能的提升。

不安全代碼#

.NET 允許你直接使用指針。這使得它類似于 C 語言。如果內循環受 CPU 限制,所有數組都可以被固定并在沒有邊界檢查的情況下訪問,或者我們可以直接像在這個 1BRC 案例中那樣直接處理本地內存。

另外,.NET 提供了一個較新的 Unsafe 類,它本質上與舊的 unsafe 關鍵字 + 指針做同樣的事情,但使用托管引用。這允許跳過固定數組,同時仍然是 GC 安全的。

不安全選項的存在并不會自動使代碼不安全。有“不安全的 Unsafe”和“安全的 Unsafe”。例如,如果我們確保數組邊界,但不能使 JIT 省略邊界檢查(如在自定義字典案例和 GetAtUnsafe 中),那么為什么我們要支付邊界檢查的成本呢?在這種情況下,它將是安全的 Unsafe。通過謹慎使用,局部不安全的代碼可以變成全局安全的應用程序。

易用的向量化函數#

.NET 有非常容易使用的 SIMD 內在函數。我們可以直接使用 SSE2/AVX2/BMI API,或者使用跨平臺跨架構的 Vector128<T>/Vector256<T> 類型。或者更通用的 Vector<T> 類型,它甚至隱藏了向量大小,并且可以在舊的 .NET 運行時上無縫工作。

.NET 的范圍#

.NET 不強迫我們每次都編寫低級的不安全 SIMD 代碼。當性能不重要時,我們可以只使用 LINQ。這很好。即使在這個 1BRC 挑戰中也是如此。真的。

C# 與 F##

F# 在默認數據集和10K數據集上都展現出了不俗的性能。我與 F# 的關系頗為復雜。博客上的一篇長篇文章講述了我為何放棄 F# 轉而選擇 C# 的原因。主要是因為性能問題(包括生成的代碼和工具的性能),盡管我喜歡 F# 的語法和社區。

然而,F# 的速度之快并不讓我感到驚訝。它一直在穩步提升,或許有一天我會再次使用 F#. 例如,可恢復代碼和可恢復狀態機是我一直在關注的非常強大的功能。.NET 原生支持的 task { ... } 計算表達式就利用了這一特性。

在這里,我不得不提到,我也通過一系列在2020年的提交,大幅提高了 F# 性能,使其核心的 Map 和 Set 數據結構(內部是 AVL 樹)的速度大大加快。

當然,正如作者所承認的,Frank Krueger 的 F# 實現遠非典型的函數式 F# 代碼。但是,如果你已經在使用 F# 代碼,而且不想碰 C#,你也可以在 F# 中寫類似 C 的代碼。只是不要過度,把它隱藏在純函數里,然后對外保密。 😉

高性能 .NET 代碼作為日常工作#

在 ABC Arbitrage,我有機會每天都處理性能關鍵的 .NET 代碼。公司多年前從 C++ 遷移到 .NET,這在可維護性、靈活性、性能和成本方面都是巨大的成功。像1BRC中看到的優化在我們的代碼中并不少見。當然,并非我們所有的代碼都是那樣的。我們還有很多易讀的現代 C# 代碼,甚至 LINQ 也不是禁止的,除非它在交易路徑上。我們總是跟上最新的 .NET 發展,通常在新的主要版本發布后幾周內就會在生產環境中使用(例如,我們已經“長時間”使用 .NET 8 了)。

我們在 ABC 使用并貢獻了許多開源項目,并且我們也維護一些。由 Olivier Coanet 維護的著名高性能線程間消息傳遞庫 Disruptor-net 的 .NET 移植版本是我們交易平臺的核心,處理著每一個市場行情和每一個交易訂單。我貢獻了一些類似上文討論的微優化。由 Lucas Trzesniewski 作為主要貢獻者的輕量級點對點服務總線 Zebus,自2013年以來一直在 ABC Arbitrage 的生產環境中運行,每天處理數億條消息,并協調整個基礎設施。由 Lucas、Romain Verdier 和其他人貢獻的日志庫 ZeroLog,速度之快且零分配,以至于我們甚至可以在最延遲敏感的路徑上輕松使用它。公司倉庫中還有其他項目,以及我們現任和前任同事的許多其他貢獻。我們真正擁抱開源 😍

如果你喜歡使用現代 .NET 編寫高性能代碼,并且想在巴黎享受一點樂趣,為何不加入我們呢?我們有幾個開放的 .NET 職位。如果你感到沖動,就在 dotnet📧abc-arbitrage.com 向我們發送申請吧。

ABC Arbitrage 是一家法國金融服務公司,專注于套利交易。成立于1995年,它主要從事股票市場的套利策略,包括統計套利、事件驅動套利和其他相關的交易策略。套利是一種利用不同市場之間的價格差異來獲利的交易策略,而ABC Arbitrage 就是在這一領域內的專家。

這家公司利用先進的數學模型和自動化交易系統來發現并執行套利機會,從而為其客戶提供收益。它可能會參與跨市場交易、跨商品交易以及其他復雜的金融衍生品交易,以期在不同金融工具之間的價格差異中獲利。

法語是美麗而有用的,但如果你英語流利,它不是硬性要求。至少對我來說是這樣。如果你不在歐盟,但想搬到那里,那么通過歐盟藍卡的法律手續非常簡單。

原文信息#

作者:Victor Baybekov

原文鏈接:https://hotforknowledge.com/2024/01/13/7-1brc-in-dotnet-even-faster-than-java-cpp/

轉自博客園https://www.cnblogs.com/InCerry/p/17964592/7-1brc-in-dotnet-even-faster-than-java-cpp


該文章在 2024/1/29 10:43:36 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved