本文章是由機器翻譯。

CLR 全面透徹解析

Silverlight 4 中的新增功能和改進的性能

Andrew Pardoe

Silverlight 4 的最大變化之一是核心執行引擎遷移到了新版 CLR 上。 從 Microsoft .NET Framework 2.0 直到 .NET Framework 3.5 SP1,.NET Framework 的每個版本均採用相同的 CLR 作為核心。 .NET Framework 4 做了一些改動,某些甚至稱得上是翻天覆地的變化,例如,分離出易於下載的用戶端設定檔,並通過優化本機二進位檔案佈局縮短啟動時間,不過我們始終採用就地更新,保持了高相容性。

有了 .NET Framework 4,我們不僅能對 CLR 自身進行較大更改,還保持了對以前各個版本的高相容性。 Silverlight 4 採用新版 CLR 作為其 CoreCLR 的基礎,並將其桌面的所有改進均應用於網路。 其中最顯著的運行時增強功能即預設垃圾收集器 (GC) 行為,以及每次執行 Silverlight 程式時不再即時 (JIT) 編譯 Silverlight Framework 二進位檔案。 我們對基類進行了全面改進,包括改進獨立存儲、修改 System.IO,以從運行的具提升許可權的 Silverlight 應用程式直接訪問檔案系統。

下麵先大致瞭解一下 CoreCLR GC 的工作原理。

分代 GC

CoreCLR 採用相同的 GC 作為桌面 CLR。 它是分代 GC,意思是其操作基於如下原則:最近分配的物件最有可能在下次收集時成為垃圾。 該方法在小範圍內是顯而易見的:函數返回後程式不能即刻訪問函數區域變數。 該方法通常也適用于較大範圍:程式往往會保留在程式執行過程中所存在物件的某些全域狀態。

物件通常在最新一代(我們稱之為第 0 代)中分配,並在垃圾收集過程中提升到上幾代(如果物件存在於收集中),直至到達最老一代(在當前 CLR GC 實現中為第 2 代)。

CLR GC 中還有一代,名為大型物件堆 (LOH)。 大型物件(當前定義為超過 85,000 位元組的物件)直接分配到 LOH 中。 該堆與第 2 代同時收集。

如果沒有分代 GC,那麼 GC 在收集未用記憶體之前,需要檢查整個堆,才能知道哪些記憶體可訪問,哪些記憶體是垃圾。 有了分代 GC,每次收集時便無需查看整個堆。 由於收集持續時間與要收集的代大小息息相關,因此我們對 GC 進行了優化,降低了第 2 代(和 LOH)的收集頻率。 對於小型堆,收集幾乎在瞬間完成,堆越大,收集時間越長,第 0 代收集只需數十微秒。

在大多數程式中,第 2 代和 LOH 要比第 0 代和第 1 代大得多,因此檢查這些堆的全部記憶體需要更長時間。 請記住,當 GC 收集第 1 代時始終會收集第 0 代,在收集第 2 代時始終會收集所有堆。 This is why a Generation 2 collection is called a full collection. 有關不同堆收集性能的詳細資訊,請參見 2009 年 10 月的“CLR 全面透徹解析”專欄:msdn.microsoft.com/magazine/ee309515

併發 GC

執行垃圾收集的簡單演算法是令執行引擎中止所有程式執行緒,然後 GC 執行其工作。 We refer to this kind of a collection as a blocking collection. 這樣一來,GC 會移動不固定的記憶體,例如,將記憶體從一代移動到下一代,或壓縮稀疏記憶體段,而程式並不知道已發生了變化。 如果在執行程式執行緒時移動了記憶體,則對程式而言,像是記憶體已損壞。

不過,也有某種垃圾收集工作不會變動記憶體。 Since the first version of the CLR, we’ve provided a GC mode that does concurrent collections. 此模式在整個收集過程中,無需中止程式執行緒便可執行大部分完整 GC 工作。

有許多功能 GC 無需更改程式可見的任何狀態即可完成,例如,GC 可以查找所有程式可訪問的記憶體。 在 GC 檢查堆時,程式執行緒可繼續執行。 在執行實際收集之前,GC 只需在檢查記憶體時搜索發生變化的記憶體,例如,如果程式分配了新的物件,則該物件需要標記為可訪問。 最後,GC 要求執行引擎攔截所有線程(就像在非併發 GC 中),並繼續完成此時的所有可訪問記憶體。

後臺 GC

併發 GC 在大多數情況下都能取得出色的效果,但還有一種情況例外,我們對此做了大幅改進。 如前所述,記憶體是在最新一代或 LOH 中分配的。 Generations 0 and 1 are located on a single segment—we call it the ephemeral segment because it holds short-lived objects. 臨時段填滿後,由於臨時段中已無空間,因此程式無法再創建新的物件。 這時 GC 需要對臨時段執行收集,以釋放部分空間,令分配繼續。

併發 GC 的問題在於,在進行併發收集的同時,無法執行以上操作。 當程式執行緒運行時,GC 執行緒不能移動任何記憶體(因而無法將舊的物件提升到第 2 代),而且,由於已存在 GC,因而無法啟動臨時收集。 但是 GC 必須在臨時段中釋放部分記憶體,程式才能繼續進行。 這樣一來便陷入了困境:不得不中止程式執行緒的原因,不是併發 GC 更改了程式可見的狀態,而是因為程式無法分配。 如果併發收集在搜索所有可訪問記憶體時發現臨時段已滿,它會中止所有線程,並執行攔截壓縮。

以上難題正是促使我們開發後臺 GC 的動因。 其工作原理類似併發 GC,始終在後臺自己的執行緒上執行大部分完整收集工作。 二者之間的主要差別在於,後臺 GC 允許在執行完整收集時進行臨時收集。 也就是說,當臨時段滿時,程式可繼續執行。 GC 只是執行臨時收集,其他一切照常運行。

後臺 GC 對程式延遲的影響十分顯著。 在運行後臺 GC 時,我們觀察到,程式執行中止次數大為減少,因而縮短了持續時間。

後臺 GC 是 Silverlight 4 的預設模式,只能用於 Windows 平臺,因為 OS X 缺乏 GC 運行于後臺或併發模式下所需的某些 OS 支援。

NGen 性能改進

託管語言(如 C# 和 Visual Basic)編譯器不會直接生成可在使用者電腦上執行的代碼。 這些編譯器將生成名為 MSIL 的中間語言,然後在程式執行時使用 JIT 編譯器編譯為可執行代碼。

使用 MSIL 從安全性到可攜性都有很多好處,但它與 JIT 編譯的代碼之間存在兩點折衷。 首先,必須先編譯大量 .NET Framework 代碼,才能編譯和執行程式的 Main 函數。 這意味著,使用者在程式開始運行之前不得不等待 JIT。 其次,對於使用者電腦上執行的每一個 Silverlight 程式,都必須編譯所使用的所有 .NET Framework 代碼。

NGen 有助於解決這兩個問題。 NGen 會在安裝時編譯 .NET Framework 代碼,這樣當程式開始執行時代碼已經編譯好。 採用 NGen 編譯的代碼一般可由多個程式共用,因而當運行兩個或多個 Silverlight 程式時,使用者電腦上的工作集將減少。 想要瞭解有關 NGen 如何改進啟動時間和工作集的詳細資訊,請參見 2006 年 5 月的“CLR 全面透徹解析”專欄:msdn.microsoft.com/magazine/cc163610

.NET Framework 代碼佔據了 Silverlight 程式的大部分,因此無 NGen 的 Silverlight 2 和 3 在啟動時間上存在明顯差距。 JIT 編譯器在優化和編譯每個程式啟動路徑上的庫代碼時,所需的時間太長。

在 Silverlight 2 和 3 中,對於此問題的解決方法是不讓 JIT 編譯器優化代碼生成。 代碼仍需編譯,但由於 JIT 編譯器只生成簡單代碼,因而不需要太少的編譯時間。 與傳統桌面應用程式相比,為大量 Internet 應用程式 Web 方案編寫的大多數程式都很小,而且執行時間短。 更重要的是,這些程式通常都是互動式程式,這意味著其大部分時間都是在等待使用者輸入。 在 Silverlight 2 和 3 應用中,快速啟動遠比生成優化代碼重要。

在 Silverlight Web 應用程式的發展過程中,我們不斷對其改進以保持積極的使用者體驗。 例如,在 Silverlight 3 中,我們增加了對從桌面安裝和運行 Silverlight 應用程式的支援。 通常情況下,相比傳統 Web 方案中的互動式小型應用程式,這些應用程式更大、功能更多。 Silverlight 自身添加了許多大計算量的功能,例如,在 Windows 7 中支援觸摸輸入,以及如 Bing 地圖網站中所示的豐富的照片處理功能。 所有這些方案都要求對代碼進行優化,以便更有效地執行。

Silverlight 4 提供了出色的啟動性能和經優化的代碼。 現在,Silverlight 中的 JIT 編譯器採用與桌面 .NET 應用程式中同樣的優化機制。 由於我們為 Silverlight .NET Framework 程式集啟用了 Ngen,因此可以執行優化。 當您安裝 Silverlight 時,我們會自動編譯 Silverlight 運行時中的所有託管代碼,並將這些代碼保存在您的硬碟中。 當使用者執行您的 Silverlight 程式時,程式無需等待任何 Framework 代碼編譯,便可開始執行。 還有一點同樣重要,我們現在可優化您 Silverlight 程式中的代碼,以便程式運行更快,並且我們可以在運行于使用者電腦上的多個 Silverlight 程式之間共用 Framework 代碼。

在安裝過程中,Silverlight 4 為 .NET Framework 程式集創建本機映射。 對於某些應用程式而言,啟動性能是唯一重要的性能。 Think of Notepad as an example: it’s important that it starts quickly, but once you start typing it doesn’t matter how fast Notepad runs (provided it runs faster than you type). 對於此類程式,JIT 編譯應用程式啟動代碼所需的時間可能會導致性能降低。 在 Silverlight 4 中,大多數應用程式的啟動要快上 400 ms 到 700 ms,在執行過程中性能最多可提高 60%。

基類庫 (BCL) 是託管 API 的核心,現在 Silverlight 4 中的 NGen 可支援該庫。 下麵來看看 BCL 中有哪些新功能。

BCL 新增功能

Silverlight 4 中許多 BCL 新增強功能也是 .NET Framework 4 的新增功能,這些在相關上下文中已有詳述。 此處只簡要介紹一下 Silverlight 4 中的新增功能。

可以使用代碼約定,在 Silverlight 代碼中以內置方式表達前置條件、後置條件和物件不變數。 代碼約定可用在代碼中更好地表達假設條件,並有助於及早發現錯誤。 使用代碼約定還有其他許多好處。 有關詳細資訊,請訪問 Melitta Andersen 2009 年 8 月的“CLR 全面透徹解析”專欄:msdn.microsoft.com/magazine/ee236408、Code Contracts DevLabs 網站 msdn.microsoft.com/devlabs/dd491992 以及 BCL 團隊博客 blogs.msdn.com/bclteam

元組最常用於返回方法的多個值, 常用在函數式語言(如 F#)和動態語言(如 IronPython)中,使用方法像 Visual Basic 和 C# 一樣簡單。 有關元組設計的詳細資訊,請參見 Matt Ellis 2009 年 7 月的“CLR 全面透徹解析”專欄:msdn.microsoft.com/magazine/dd942829

Lazy<T>為延遲初始化物件提供了便捷方法。 應用程式可以使用延遲初始化技術將資料的載入或初始化推遲至首次需要資料時。

Silverlight 4 SDK 在 System.Numerics.dll 中新增了 BigInteger 和 Complex 數值資料類型。 BigInteger 用於表示任意精度的整數,Complex 用於表示帶實數和虛數部分的複數。

Enum、Guid 和 Version 現在也像大多數其他 BCL 資料類型一樣支援 TryParse,從而可以更有效地根據字串創建實例而不會引發異常或錯誤。

Enum.HasFlag 是新增的一個便捷方法,用於輕鬆檢查 Flags 枚舉是否設置了標誌,從而無需記住如何使用位運算子。

String.IsNullOrWhiteSpace 是用於檢查字串是否為 Null、為空或只包含空白的便捷方法。

String.Concat 和 Join 重載現在採用 IEnumerable<T>參數。 這些新的 String.Concat 和 Join 重載可將實現 IEnumerable<T>的任意集合連接在一起,而無需先將集合轉換為數組。

Stream.CopyTo 簡化了在一行代碼中從一個流中讀取內容然後寫入另一個流的操作。

除上述新功能之外,我們還對獨立存儲進行了改進,並啟用了受信任的 Silverlight 應用程式,以通過 System.IO 直接訪問檔案系統部分。

獨立存儲增強功能

獨立存儲是一個虛擬的檔案系統,Silverlight 應用程式可通過它在用戶端上存儲資料。 有關 Silverlight 中獨立存儲的詳細資訊,請參見 2009 年 3 月的“CLR 全面透徹解析”專欄:msdn.microsoft.com/magazine/dd458794

Silverlight 4 中獨立存儲最顯著的改進是性能方面。 自 Silverlight 2 發佈以來,我們從開發人員那收到了許多有關獨立存儲性能的回饋。 在 Silverlight 3 中,我們做了一些改進,大大提高了獨立存儲的資料讀取性能。 在 Silverlight 4 中,我們更進了一步,解決了開發人員在對獨立存儲寫入資料時遇到的性能瓶頸。 總之,獨立存儲的性能在 Silverlight 4 中有了大幅提高。

另外,開發人員還曾反映,獨立存儲中沒有重命名或複製檔的簡單方法。 要重命名檔,不得不手動讀取原始檔、創建並寫入一個新檔,然後刪除原始檔。 重命名目錄時也只能採取類似的方法,甚至需要更多代碼行,尤其是當要重命名的目錄中還包含子目錄時。 這種方法雖然可行,但需要編寫較多代碼,而且也不如直接告知 OS 重命名磁片上的檔或目錄那麼有效。

在 Silverlight 4 中,我們在 IsolatedStorageFile 類中增加了一些新方法,調用這些方法後,只需一行代碼即可高效執行以上操作:CopyFile、MoveFile 和 MoveDirectory。 我們還增加了以下新方法,以提供有關獨立存儲中檔和目錄的更多資訊:GetCreationTime、GetLastAccessTime 和 GetLastWriteTime。

我們在 Silverlight 4 中新增了另一個 API,IsolatedStorageFile.IsEnabled。 在以前的版本中,要確定獨立存儲是否啟用的唯一方式是嘗試使用獨立存儲,然後捕獲後續的 IsolatedStorageException,如果獨立存儲禁用則會引發該異常。 現在,可以使用新的 IsEnabled 靜態屬性來輕鬆確定獨立存儲是否啟用。

如今,很多流覽器,如 Internet Explorer、Firefox、Chrome 和 Safari,在流覽歷史記錄、Cookie 和其他不保留的資料時都支援隱私流覽模式。 因此,當流覽器處於隱私模式下時,Silverlight 4 支援隱私流覽設置,可防止應用程式訪問獨立存儲並將資訊存儲在您的本地電腦上。 在這種情況下,IsEnabled 屬性將返回錯誤,且任何使用獨立存儲的企圖都將引發 IsolatedStorageException,當使用者明確禁用獨立存儲時同樣如此。

檔案系統訪問

Silverlight 應用程式運行于部分信任的安全沙箱中。 安全沙箱限制了對本地電腦的訪問,並對應用程式設置了許多約束,以防止惡意程式碼造成傷害。 例如,部分信任 Silverlight 應用程式無法直接訪問檔案系統。 如果應用程式需要在用戶端上存儲資料,唯一的方法是將資料存儲在獨立存儲中。 要訪問範圍更大的檔案系統,只能通過 OpenFileDialog 或 SaveFileDialog 來實現。

Silverlight 3 增加了在流覽器之外安裝和運行應用程式的功能。 這產生了一些有趣的離線方案,不過此類應用程式仍舊運行在與運行于流覽器內的應用程式同樣的沙箱中。 Silverlight 4 允許流覽器外的應用程式將自身配置為運行于提升的信任等級下。 此類受信任的應用程式在安裝後可以繞過沙箱的某些限制。 例如,受信任的應用程式可以訪問使用者檔、不受跨域訪問限制地使用網路、繞過使用者同意和初始化要求、訪問本機 OS 功能等。

使用者在安裝需要提升的信任等級的應用程式時,正常安裝提示將被一條警告資訊取代,警告使用者該應用程式可以訪問使用者資料,只應當從受信任的網站進行安裝。

受信任的應用程式可以在 System.IO 中使用 API,以直接訪問檔案系統中的以下使用者目錄:MyDocuments、MyMusic、MyPictures 和 MyVideos。 目前還不允許對以上目錄之外的目錄進行檔操作,否則將導致 SecurityException。 對於這些目錄,允許執行包括讀和寫在內的一切檔操作。 例如,受信任的相冊應用程式可以直接訪問 MyPictures 目錄下的所有檔。 受信任的視頻編輯應用程式可以將電影保存到 MyVideos 目錄中。

由於這些目錄的檔案系統路徑因不同基礎 OS 而異,因此在應用程式中不應對這些路徑進行硬編碼。 Windows 與 Mac OS X 的檔案系統路徑截然不同,Windows 各個版本之間的路徑也可能不同。 為在所有平臺上正常工作,應當使用 System.Environment.GetFolderPath 獲取這些目錄的檔案系統路徑。 以下代碼使用 Environment.GetFolderPath 獲取 MyPictures 目錄的檔案系統路徑,並使用 System.Directory.EnumerateFiles 方法在 MyPictures(及其子目錄)中查找尾碼為 .jpg 的所有檔,然後將每個檔路徑添加到 ListBox 中:

if (Application.Current.HasElevatedPermissions) {
  string myPictures = Environment.GetFolderPath(
    Environment.SpecialFolder.MyPictures);
  IEnumerable<string> files = 
    Directory.EnumerateFiles(myPictures, "*.jpg", 
    SearchOption.AllDirectories);
  foreach (string file in files) {
    listBox1.Items.Add(file);
  }
}

以下代碼顯示了如何從受信任的應用程式,在使用者的 MyDocuments 目錄中創建一個文字檔:

if (Application.Current.HasElevatedPermissions) {
  string myDocuments = Environment.GetFolderPath(
    Environment.SpecialFolder.MyDocuments);
  string filename = "hello.txt";
  string file = Path.Combine(myDocuments, filename);

  try {
    File.WriteAllText(file, "Hello World!");
  }
  catch {
    MessageBox.Show("An error occurred.");
  }
}

System.IO.Path.Combine 用於將 MyDocuments 的路徑與檔案名結合在一起,並在二者之間插入基礎平臺相應的目錄分隔符號(Windows 採用 \,Mac 採用 /)。File.WriteAllText 用於創建檔(如果檔已存在,則覆蓋),並將語句“Hello World!”寫入該檔。

更優性能和更多功能

如上所述,Silverlight 4 中的新版 CLR 對運行時和基類都做了改進。全新的 GC 行為、Silverlight Framework 程式集中的 Ngen,以及增強的獨立存儲性能,這一切都表明 Silverlight 4 上的應用程式將啟動更快、運行更出色。BCL 增強功能使應用程式只需更少代碼即可執行更多功能,並且,諸如受信任的應用程式可訪問檔案系統等新功能推動了更多優秀應用程式方案的湧現。

Andrew Pardoe 是 Microsoft 的 CLR 專案經理。其工作領域涉及桌面和 Silverlight 運行時執行引擎的多個方面。您可以通過電子郵寄地址與他聯繫:andrew.pardoe@microsoft.com

Justin Van Patten 是 Microsoft CLR 小組的專案經理,主要從事基類庫的研究。您可以通過 BCL 小組博客與他聯繫:https://blogs.msdn.com/b/bclteam/

衷心感謝以下技術專家對本文的審閱:Surupa Biswas、Vance MorrisonMaoni Stephens