C#比C ++慢嗎?這是一個很大的問題。作為初級開發人員,我確信答案是“肯定的”。現在,我經驗豐富了,我知道這個問題並不明顯,甚至很複雜。
在嘗試回答這個問題之前,還有另一個問題:“真的重要嗎?”。對於現代CPU,開發效率應該比性能重要得多,對嗎?而且用C#編寫的代碼比用C ++編寫的代碼生產力要高得多,所以這並不意味著我們要切換到C ++只是因為它快一點。
我認為這確實很重要。詳細瞭解答案將有助於改進兩種語言。您可能會學習如何編寫性能更高的C#代碼。或者,我們可能會改進C ++語言及其庫以提高生產力。或者,您可以將對性能敏感的熱路徑移至C ++,通過互操作從C#調用此代碼。另外,如果兩種語言都具有相同的性能,則可以首先避免很多不必要的工作切換到C ++。除此之外,回答這是一個非常有趣的練習。
託管代碼與本機代碼
比較C#和C ++會導致一個更普遍的問題:“託管代碼是否比本地代碼慢?”。兩者之間到底有什麼區別?有幾個區別。其中之一是將本機代碼(例如C或C ++)直接編譯為機器代碼,可以由計算機執行。另一方面,託管代碼首先被編譯為“中間”代碼。那是Java中的字節碼和C#中的中間語言(IL)代碼 。然後,在運行時,即時(JIT)編譯器將該代碼編譯為機器代碼。這樣做的原因是同一代碼可以在不同的機器上以不同的方式編譯。因此,例如,可以將相同的IL代碼編譯到Windows和Linux。
另一個很大的不同是內存管理,但讓我們先從IL代碼的含義開始。
IL代碼與機器代碼
在託管代碼中,運行時還有另一個編譯階段。IL代碼編譯為機器代碼。難道這意味著託管代碼總是比本地代碼慢嗎?好吧,默認情況下,是的。首次在C#中調用方法時,它將JIT編譯為機器代碼。這需要時間。但是,每種方法只發生一次。這就是.NET進程的啟動時間將比C ++進程的啟動時間更長的原因。好吧,至少是原因之一。
但是,啟動時間並不是一個大問題,原因如下:在服務器中,啟動時間並沒有真正起作用。服務器運行時間很長,可以忽略不計。即使一天要部署幾次,也可以部署到暫存環境,等待啟動完成,然後在DNS級別“交換”暫存和生產。
對於桌面應用程序和移動設備,啟動時間確實是一個很大的問題。但是,這可以通過一種稱為
“時間提前(AOT)編譯”的技術來解決。在這裡,您可以在運行之前將IL代碼編譯為機器代碼。在C#中,這是通過稱為Ngen.exe的工具完成的,該工具應在用戶的計算機上執行。通常,這是安裝程序中的另一步驟。對於移動設備,JIT編譯不是一個選擇。有一些設備限制不允許這樣做,因此AOT編譯是必需的。這在Mono運行時中已經完成,並通過一項稱為ReadyToRun Images(R2R)的技術添加到.NET Core中。因此,如果JIT編譯不是問題,那麼C#代碼和C ++代碼應該以相同的速度運行,對嗎?畢竟,這是相同的機器代碼指令。JIT編譯問題只是其中的一部分。您認為哪種代碼會產生更好的機器代碼?將C ++代碼直接編譯成機器代碼?還是將C#代碼編譯為IL代碼然後再轉換為機器代碼?哪個將更優化且指令更少?
假設C#編譯器和C ++編譯器都進行了出色的優化。實際上,我認為這非常接近事實。現在考慮C#代碼實踐。我們經常使用諸如LINQ,異步/等待狀態機,模式匹配和反射之類的東西。這些都是有用的和富有成效的功能,但也很慢。在C ++中,我們通常將代碼編寫得更接近於機器代碼。我們直接使用內存指針,並且幾乎沒有高級抽象。因此,即使可以用C#編寫低級代碼,也可以用C ++編寫高級代碼(現在有很多庫,包括C ++ LINQ),大多數C ++代碼將以更少的指令生成更快的機器代碼。
總之,從理論上講,您可以創建與C ++代碼一樣快的C#代碼。但是,在大多數情況下,由於編碼習慣,C ++代碼將變得更快。差異通常並不重要,但在熱路徑和算法中確實很重要。因此,作為C#開發人員,確保優化性能敏感的代碼。
內存管理
託管代碼的另一個方面是內存管理。在C#等託管語言中,公共語言運行時(CLR)對內存完全負責。它將為每個分配查找內存空間,並在不再引用時自動刪除內存。這稱為垃圾收集。垃圾收集器還會移動內存以防止碎片問題。那是您多次分配和刪除內存的時間,直到內存緩衝區中出現“空洞”。這使分配新內存的速度變慢,即使有足夠的可用內存,最終也可能導致內存不足崩潰。在C#中,垃圾回收器不斷地將內存緩衝區彼此相鄰移動。這樣,它確切地知道了在哪裡分配內存,不需要尋找空閒的“空洞”(儘管大對象堆仍然存在碎片問題)
在本地語言中,分配和釋放是由程序員控制的。一條new語句直接在堆上分配代碼,並delete釋放內存。由於內存從未像.NET那樣“移動”,因此效果很好。除了它為人為錯誤創造了很大的空間。如果無法刪除內存,它將被永久佔用並導致內存洩漏。如果有足夠的內存洩漏,您將開始遇到性能問題,並最終崩潰。但是我們在這裡是在理論上進行討論,因此讓我們假設所有C ++程序員都創建了完美的代碼,而這些代碼永遠不會發生內存洩漏。
那麼C ++代碼或C#代碼哪個更快?好吧,垃圾回收雖然很棒,但是卻需要時間。在垃圾回收期間,線程通常必須暫停執行。因此,在您的流程中運行的代碼會執行垃圾回收邏輯,而不是執行用戶代碼。話雖如此,垃圾收集器已進行了優化。在最新版本的GC中,它甚至可以在另一個線程中在後臺運行,而絲毫不影響性能(有時)。
因此,要問的問題是,垃圾回收開銷是否會比碎片問題開銷更多地損害性能?在短期內,當應用程序剛剛開始運行時,碎片化就不是問題,垃圾回收肯定是問題。因此,C ++在程序啟動時肯定更快。從長遠來看,當您的應用程序連續運行數小時和數天時,碎片問題將迎頭趕上。分配將變慢,並且在某些情況下,它將導致崩潰。
C ++開發人員知道此問題並有解決方法。一種解決方案是自定義分配器,它可以重用內存緩衝區或巧妙地分配以最大程度地減少碎片問題。
另一個解決方案是儘可能多的在堆棧上分配對象而不是堆。堆棧存儲器從定義上看沒有碎片問題,並且還有其他一些好處。在堆棧上使用對象比在堆上快得多。無需讀取內存引用,然後去實際對象的那個位置。由於CPU高速緩存,堆棧存儲器的工作效果更好。另一個好處是您不必手動刪除對象,因此沒有人為錯誤的地方。
在C ++中,分配堆棧非常有效,但在C#中則不是。在C ++中,您可以在堆棧上分配任何對象,無論是類還是結構。您可以輕鬆地通過引用傳遞堆棧分配的對象。在C#中,類型定義是在堆還是在堆棧上分配。像struct這樣的值類型分配在堆棧上,而像類這樣的引用類型則分配在堆上。像數組這樣的集合List在堆上分配。儘管從C#7.3開始,您可以使用new stackalloc關鍵字在堆棧上分配數組並通過引用傳遞它們。就性能而言,“堆棧與堆”問題是如此重要,以至於最新的C#版本已投入大量資金來允許更多的堆棧分配並通過引用傳遞這些對象。儘管如此,C ++的指針仍然更適合於此。
因此,C ++在堆棧分配方面更好,並且沒有垃圾回收開銷。另一方面,C#沒有碎片問題。除了生產力問題之外,我認為C ++在這方面的整體速度更快。主要是由於堆棧分配,而不是因為缺少垃圾收集器。C#在新功能stackalloc和Span功能方面正在迎頭趕上。
摘要
我在這篇文章中的思考過程可能不會描述C#和C ++性能的所有方面。我敢肯定雙方都有更多論點(我希望您能發表評論並分享這些觀點)。而且,在性能方面,任何論點都只是猜測,沒有基準來支持它。在這種特定情況下,創建正確的基準非常困難,因為要比較的場景和事物太多了。
我的結論仍然是,默認情況下,C#在大多數情況下不如C ++快。但是我認為它並不會慢很多,通常也沒關係。當您確實具有對性能敏感的代碼時,可以優化C#並獲得與C ++幾乎相似的性能。這可以通過堆棧分配來完成,從而避免了LINQ,重複使用內存和許多其他性能優化。
閱讀更多 非科班碼農 的文章