C++是一種眾所周知的程式語言。 這種聲譽好壞參半。 從好的方面來說,C++是如此之好,以至於如果你想玩一門好的程式語言,你忍不住要與C++正面交鋒。壞的一面是,它是出了名的複雜,難以學習,難以使用。
無論C++是好是壞,不可否認的是,C++仍然是一種非常流行和動態的語言。 在沉寂了十多年之後,語言標準的第二個版本 - C++11 - C++每三年發布一次新的語言標準,每個版本都提供改進和新功能,同時保持基本的向後相容性。
雖然有像 Rust 這樣的新語言在語言領域挑戰 C++,但不可否認的是,C++ 仍然是面向效能的世界中程式語言之王。 我什至不認為 C++ 在效能方面不如 C——C++ 在追求極速方面可以比 C 更強,而 C 相對於 C++ 的主要優勢在於它更簡單:無論是學習、使用還是產生二進位**卷。
今天,我們將討論C++是如何如此高效能的。
根據Bjarne的說法,C++的主要特點是以下兩個問題:
就像 C 一樣,C++ 提供了它非常低的水平操作資料的能力為開發人員提供了靈活性。 像“高階”語言一樣,C++提供了乙個強大的抽象(可以說比大多數語言更多)。 而且,與 C 相比,C++ 確實如此更安全。這在語言的早期是正確的,更不用說現在了。
C++ 的型別系統比 C 更具限制性,因此,儘管一直有人聲稱 C++ 是 C 的超集,但這從來都不是嚴格意義上的。 最近(2023 年)發生了一起程式崩潰的案例,簡而言之,開發人員使用了乙個二維的 char 陣列(char names[max names] [max name len])並將其傳遞給函式......接收 char** 引數這當然是錯誤的,但是C編譯器發出了警報,但編譯仍然沒有失敗。 如果這是 C++,編譯器將只報告錯誤,而不會傳遞它。
第二點,零開銷抽象,這對 C++ 的效能至關重要。 我們有很多抽象,使用它們沒有額外的開銷。 在某些情況下,使用這些機制會產生“負開銷”——使用者“可以非常安全地使用該語言,並且可以獲得非常高的效能”。 同時,C++也賦予了“定製者”根據自身需求編寫更接近用例的庫的能力,可以進一步方便“使用者”。
當然,定製需要程式設計師非常高的技能水平。 C++初學者需要掌握C++標準庫的使用——如果你用好標準庫,你可以得到很好的效能。 正如 Gartner 名言的完整版本:
C++ 已經提供了相當多的機制,使我們能夠輕鬆實現高效能,在許多場景中遠遠超過 Gartner 的 12%。
例如,C++ 標準庫的 sort 和 C 標準庫的 Qsort :當優化關閉時,在測試場景中會得到 1:25.效能差異,C++似乎慢了很多; 但是一旦 -o2 被開啟(允許內聯),兩者之間的效能差異就會變為 35:1,C++比C好幾倍! 這稱為“負開銷”。 C++ 比 C 更簡單、更直觀、效能更高。 這樣做的原因是 C++ 的函式物件和模板機制允許編譯器更好地內聯,從而獲得更高的效能。
因此,學習好使用C++的第一步是利用好C++和標準庫的基本機制,了解標準庫不同機制的效能成本,包括時間和空間。
在任何情況下,學習C++首先需要了解的是析構函式和RAII(資源獲取即初始化)習語。 是的,雖然C++誕生於“帶類的C”,但類並不等同於物件導向,對物件導向程式設計的支援也不是C++最重要的特性。 C++ 自定義型別的特別之處不在於多型性,而在於自定義它們的行為——最重要的是,當物件被銷毀時應該做什麼。 析構函式和析構函式帶來的 RAII 習語是 C++ 最重要的特性,也是 C++ 中資源管理的關鍵。
過載是另乙個非常重要的C++功能。除了不必區分程序字元、程序字串和程序 int 的便利之外,它對於泛型程式設計和移動語義也很重要,這是現代 C++ 的基本特徵。 剝離了語法細節,移動語義本質上使程式設計師很容易區分將繼續使用的物件和將來不會使用的物件,從而允許後者通過使用建構函式和賦值運算子的過載而被“竊取”。 對於普通向量,複製成本為 o(n) 或更高(如果向量成員是容器或其他複製成本高的物件),但移動成本通常為(是的,只是通常; 但是,您通常也不會遇到此異常)是 o(1),恆定複雜度。這是我們在 C++ 中高效傳遞物件的常用方法。
C++ 標準庫中最常見的元件可能是字串和容器。 它們都針對運動進行了優化。 當然,除了這個基本的效能點之外,容器還有自己特殊的效能點,比如不同情況下插入效能的差異。 這些都是需要學習的領域。
例如vector尾部的插入效能更好,中間的插入效能更差。 但是,進一步了解,您需要知道,良好的尾部插入效能的先決條件是元素型別具有良好的運動實現,並且宣告了運動建構函式noexcept!如果你實現了乙個帶有 o(1) 開銷的運動建構函式,但忘記將其宣告為 noexcept,那仍然是乙個 noexcept,並且向量的尾部插入仍然存在效能問題。
例如,無論列表是從開頭、結尾還是中間插入,它們都具有很高的效能。 但是,對於同一元素的列表和向量,列表的遍歷性能可能會差乙個數量級。 其原因並不完全是C++知識,而是與硬體的快取組織有關。 如果我們關心效能,這些都是需要知道的事情。
我們已經提到了模板,但字串和容器都是模板,可以使用模板引數自定義行為,並允許有效的內聯優化。 當然,模板是 C++ 中比較複雜的部分之一,但基礎知識相當簡單:向量是帶有 int 的向量,它與普通類沒有什麼不同——只是模板建立者不必為不同的型別手動建立不同的類。
很好地使用 C++ 並在您的專案中獲得令人滿意的效能當然不是上面提到的唯一乙個。 在最基本的層面上,我們還需要了解標準的庫演算法,並適當地使用併發性和並行性,以充分利用硬體。 我們就這樣吧。
當我們熟悉了C++之後,我們就會逐漸不再滿足於C++標準庫的“標準**”,我們會尋找自己的第三方庫,甚至自建輪子來滿足專案的特定需求。此時,我們需要了解有關 C++ 高階功能的更多資訊。 我們需要了解有關模板的更多詳細資訊,尤其是專業化。 我們需要了解 SFINAE 和模板元程式設計。 我們需要了解 constexpr 以及它為編譯時程式設計帶來的便利性。 C++ 使用者可能暫時不關心這些問題,但定製人員或專案中的開發者和工具構建者必須了解 C++ 的這些高階功能,才能為您的專案提供堅實的基礎。
例如,C++的標準庫提供了列表,雙向鍊表。 這個庫沒有錯,但在某些用例中,它的時間和空間開銷並不令人滿意,例如我們的物件除了正常管理之外,還需要乙個額外的 LRU(最近使用最少)演算法來丟棄其中最古老的演算法。 當然,您可以使用列表,但每次插入物件時都需要插入乙個物件,並且除了堆記憶體分配開銷之外,還需要考慮列表中的確切內容。 也許有乙個智慧型指標? 情況是否越來越複雜?
在這種情況下,最合理的選擇是使用某種侵入式列表,這是一種侵入式鍊表,不需要在每次插入或刪除時進行記憶體管理。 C++ 標準庫不提供此功能。 您可以使用 boost 中提供的容器,也可以編寫乙個新的容器。 對於這個例子,boost可能已經足夠了。 但總會有一些現成的庫無法解決的問題,使用 C++ 的高階功能來構建自己的輪子是很自然的。 我們可以自定義它,並像使用現有容器一樣使用它,而無需額外的學習成本。
或也許您希望使用分配器建立容器記憶體池,以提供記憶體的有效利用。 這在 C++ 中也很容易實現,只要您了解適當的自定義機制。 根據洋蔥原理,可以忽略這些自定義點,只使用 C++,這是最簡單的;你也可以“切”標準庫,用自己喜歡的方式使用——當然,這種方法真的和切洋蔥一樣,很容易哭。 但它確實可以幫助您獲得盡可能高的效能。
這就是這次的全部內容