C++/CX性能陷阱

jopen 11年前發布 | 12K 次閱讀 C/C++

  英文原文:C++/CX Performance Pitfalls

  使用C++/CX 編寫應用程序和編寫正常的 C++ 應用程序不一樣。純 C++ 代碼和 Windows 運行時(WinRT)之間的互操作性出奇的昂貴。基于 Sridhar Madhugiri 的視頻 C++/CX 最佳實戰中的內容,我們在本文中列舉了一些在 Windows 8 開發中避免性能問題的方式。

  邊界

  在應用程序的邊界上會產生多種性能障礙。

  數據轉換就是其中的一個例子。考慮一下一個 Web 服務客戶端和應用程序剩余部分之間的典型邊界。大多數 Web 服務是使用 UTF-8 編碼的,而大多數 Windows 應用程序的內部則是使用 UTF-16 編碼的。在 Windows 中 UTF-16 編碼是如此的流行以致于人們有時會將它錯誤地稱為“Unicode”編碼。數據轉換的成本可能是確定的,也可能廣泛變化,這依賴于它在數據本身中的特定值。

  下一種性能消耗來自于類型轉換。例如,你可能需要一個 wstring,但是卻有一個 wchar_t *。盡管在內存中每種類型所包含的數據看起來是一樣的,但是將這些內容從一個數據結構復制到另一個數據結構依然是有性能成本的。

  最后一種性能消耗來自于數據復制操作。有時候你必須為邊界處的數據復制付出代價,哪怕它們并不需要數據轉換和類型轉換。

  我們為什么要在現在討論這些內容呢?原因是 WinRT 本身就是應用程序和操作系統其余部分之間的邊界。編寫高性能C++/CX 應用程序的本質就是識別邊界并在可能的情況下避免跨越邊界。

  如果跨越 WinRT 邊界的操作無法避免,那么就尋找一些方式減少數據復制、類型轉換和數據轉換操作的數量。例如,如果數據源和目標都使用 UTF-8 編碼,那么就沒必要將數據轉換為 UTF-16,因為你最終還是需要將其再轉換回來。

  字符串

  在大多數應用程序中字符串都是主要的數據類型。文件系統、Web 服務、UI、消息、符文和契約等領域對字符串的依賴性日益加深。不幸的是人們所使用的字符串類型非常多。

  在內部,大多數應用程序可能會使用 std::wstring 或者 std::wchar_t*,你所依賴的大多數第三方類庫也是如此。但是在與 WinRT 類庫進行通信的時候你需要切換到 Platform::String^。每一次轉換都需要一次內存分配和一次數據復制操作。

  String^和本地 C++ 版本之間的一個關鍵區別是:String^是不可變的。WinRT 運行時對不可變字符串的這種強調可能來自于 .NET 和 CLR。正如^符號所表示的,String^也是引用計數。

  人們可能會對可變和不可變字符串相關的優點爭論一整天,但是最終只有一個事實。因為 C++ 標準類庫只理解可變字符串,而 WinRT 僅理解不可變字符串,所以對這兩者你都必須進行處理。正如前面所提到的,這意味著需要對字符串進行復制。

  類庫作者:如果你正在構建一個一般用途的類庫供他人使用,那么你應該考慮提供多個不同版本的 API,為每種字符串類型提供一個 API。這樣你就不需要猜測 API 的使用者在調用類庫的時候使用的是哪種字符串類型了。

  很多基于字符串的操作實際上并不需要使用字符串,但是開發者寧可選擇使用字符串迭代器。因為可變和不可變數據結構的迭代操作是一樣的,你可以在使用常規 xxx_iterator ( begin (string), end (string), …)語法的字符串平臺上直接創建 STL 樣式的迭代器。

  另外,首先要查找直接返回 wchar_t*的 API,而不是將它封裝成一個 wstring。如果你找到了這樣的 API,那么你就能夠通過數組中第一個元素的地址以及數組的長度創建一個新的 platform string。這樣就不需要創建一個在匹配的 platform string 被創建之后立即就會被廢棄的 wstring。

  調用帶有字符串引用(StringReference )類型輸入參數的 WinRT API 時有一個小竅門。你可以向一個參數類型為 platform string 的 WinRT 函數傳遞一個 wchar_t* 或者 wstring 參數,這種情況下將創建一個輕量級外觀。無論如何,這里有一些需要注意的地方。

  1. 字符串必須是空終止否則將會拋出一個錯誤。
  2. 如果字符串在函數之外的任何地方發生了變化,那么結果將無法確定。
  3. 如果函數之內有任何字符串的引用,那么無論如何都會生成一個完整副本。
  4. </ol>

      上面的第 1 條內容很容易驗證,第 2 條則僅會在碰見線程安全問題的時候發生。在大多數環境下這應該是一個非常有用的技巧。

      類庫作者:為了確保上面的方案是真實可能的,首先盡量避免讓它引用你以 StringReference 參數的方式獲取到的字符串。因為隨后的引用并不會引入額外的復制,所以不要擔心使用第二個引用。

      集合

      與 C++ 中常見的集合相比,WinRT 中的集合是非常昂貴的。和 .NET 中可觀察的集合一樣,對 WinRT 集合的每一次修改都會產生一個通知。該通知主要用于 XAML 數據綁定以便于更新 UI。

      在初始化期間避免這種損失的一種方式是,首先在堆棧上創建并填充一個標準的 vector,然后使用 move 函數初始化一個 platform vector。你能夠這樣做,因為標準的 vector 將會被銷毀,同時它的動態內存無論如何都會被釋放。

      在更新很多元素的時候,考慮使用 ReplaceAll 方法。這僅會觸發一個通知而不是每一條記錄一個通知。在 WPF 和 Silverlight 中沒有與之相對應的方法,因為這些 UI 堆棧本身不支持一次性插入或者移除多個條目。

      WinRT 集合中的另一種性能消耗來自于元素的讀取。WinRT 集合是以接口的形式暴露的,因此它們是虛的,這就意味著它們并不能像普通的函數那樣被內聯。此外,每一次讀取都需要進行范圍檢查。所以如果你需要多次讀取同一個值,考慮將它復制到一個局部變量中,不要每次都從集合中讀取。實際上復制的缺點是,你必須復制值或者增加對象上的引用數,這是一個連鎖操作。

      完全避免這種消耗的一種方式是在迭代集合之前復制它。分配一個正確大小的局部 vector,然后在 ArrayReference 上使用 GetMany 函數。然后結合使用 ReplaceAll 方法,你就能夠對集合進行幾次迭代,僅需要跨越 WinRT 邊界三次就能夠做一系列復雜的修改。

      WinRT 接口

      和傳統的 COM 一樣,在 WinRT 中一個對象的成員僅會通過接口暴露。你永遠都不可能直接訪問對象。C++/CX 通過做必要的隱式轉換對你隱藏了這些細節。這樣做之所以必要的一個常見原因是,可以滿足調用非默認接口上的方法時的需要。

      WinRT 中的轉換不是廉價的。它需要調用 QueryInterface 這個虛方法,同時有一個增加引用數的連鎖操作。一旦完成了對非默認接口的調用,還需要另一個減少引用數的連鎖操作。

      類庫作者:

      確保類中所有的常用方法在默認接口上都是可用的,這樣就不需要轉換成另一個接口了。

      如果要對同一個非默認接口進行多次調用,那么創建一個該接口類型的局部變量。這樣僅需要執行一次轉換,而不是每次方法調用時都做一次轉換。

      

      在任何可能的時候你都應該使用堆棧分配或者 unique_ptr 類。因為這樣你將獲得所有選項的最好性能。

      在你確實需要一個復雜生命周期的時候,你的下一個選擇是通過一個 shared_ptr 訪問的普通 C++ 類。這種方式和上面選項之間的主要區別在于引用數開銷。

      你選擇的最后一種手段應該是 ref 類。一個 ref 類擁有和 shared_ptr 相似的引用數語義,但是能夠帶來其他基于 WinRT 的開銷。所以僅在需要將類傳遞到一個 WinRT 函數或者在 XAML 的數據綁定中使用 ref 類。

      在使用 ref 類的時候,盡量保持較淺的繼承層次。WinRT 繼承和 C++ 繼承不一樣,它有額外的開銷。

      XAML 數據綁定

      在 WinRT+XAML 數據綁定中你應該避免實現 INotifyPropertyChanged,除非你確實希望在屬性被填充之后發生改變。同樣的,不要暴露公共 set 函數,除非 UI 確實需要修改數據。

      XAML 所調用的 get 函數應該是廉價的。不僅僅是因為它們是在 UI 線程上被調用的,還因為我們可能會調用它們多次。所以不要在 get 函數中分配內存或者執行昂貴的計算。

      對于所有基于 XAML 的 UI(WPF、Silverlight、WinRT+XAML)而言,一點非常重要的建議是保持較淺的數據層次。綁定表達式中的每一個點都代表了一次屬性改變事件,而為了保持屏幕及時更新數據綁定引擎必須監聽這些事件。

    來自: InfoQ
     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!