次の方法で共有


CLR 徹底解剖

Silverlight 4 の新機能とパフォーマンスの向上

Andrew Pardoe

Silverlight 4 の最大の変更点の 1 つは、中核となる実行エンジン用の CLR が新しいバージョンに移行されたことです。Microsoft .NET Framework 2.0 から .NET Framework 3.5 SP1 までの .NET Framework のすべてのリリースでは、その中核に同じ CLR を使用してきました。.NET Framework 4 ではいくつか変更が加えられました。たとえば、ダウンロードが容易な Client Profile を要素から取り除いたり、ネイティブ バイナリのレイアウトを最適化して起動時間を短縮したりといった、大きな変更もいくつか加えられています。しかし、このフレームワークは、インプレース更新されることから、常に、高い互換性が求められるという制限が課せられています。

.NET Framework 4 のリリースでは、以前のバージョンと高い互換性を保ちながらも CLR 自体を大きく変更することに成功しました。Silverlight 4 では、CoreCLR の基盤として新しい CLR を使用するため、デスクトップから Web に至るまで、この新しい CLR で行われた機能強化がすべて利用できるようになります。ランタイムに関する最も重要な機能強化は、既定のガベージ コレクター (GC) の動作が変更されたことと、Silverlight プログラムを実行するたびに Silverlight Framework のバイナリに Just-In-Time (JIT) コンパイルされなくなったことです。基本クラスにも徹底した強化が行われ、分離ストレージが強化され、実行中の Silverlight アプリケーションから権限を昇格して直接ファイル システムにアクセスできるように System.IO に変更が加えられています。

ではまず、CoreCLR GC が機能するしくみについての簡単な背景知識から説明しましょう。

世代別の GC

CoreCLR は、デスクトップ CLR と同じ GC を使用します。これは世代別の GC で、"最後に割り当てられたオブジェクトは次回のガベージ コレクション時にガベージ コレクションの対象になる可能性が最も高い" というヒューリスティックに基づいて機能します。このヒューリスティックは、小さなスコープで明らかで、関数から戻った直後には、プログラムからその関数のローカル変数にアクセスすることはなくなります。このヒューリスティックは、一般にスコープが大きくなっても当てはまります。通常、プログラムは、プログラムの実行中に存在するオブジェクトのグローバル状態を保持しています。

一般に、オブジェクトは最も若い世代 (世代 0) に割り当てられます。このオブジェクトが、ガベージ コレクション中にコレクションの対象にならなかった場合は、1 つ古い世代に昇格します。このような昇格は、オブジェクトが最も古い世代 (CLR GC の現在の実装では世代 2) になるまで繰り返されます。

CLR GC には、大きなオブジェクト ヒープ (LOH) という別の世代も用意されています。大きなオブジェクト (現在の定義では 85,000 バイトを超えるオブジェクト) は、LOH に直接割り当てられます。このヒープは、世代 2 と同時にガベージ コレクションが行われます。

世代別 GC がなければ、使用されていないメモリのコレクションを行う前に、アクセスできるメモリとガベージ コレクションの対象となるメモリを把握するために、ヒープ全体を調査する必要があります。世代別 GC があれば、コレクションのたびにヒープ全体を調べる必要はありません。ガベージ コレクションにかかる時間はコレクションの対象となる世代のサイズに直接関係するため、世代 2 (および LOH) だけはコレクションの頻度が少なくなるように GC が最適化されます。ガベージ コレクションは、小さなヒープではほぼ一瞬で完了し、ヒープが大きくなるほど長い時間がかかります。世代 0 のコレクションでは、数十ミリ秒しかかからないこともあります。

ほとんどのプログラムでは、世代 2 と LOH は世代 0 と世代 1 に比べてはるかにサイズが大きくなるため、これらのヒープのメモリをすべて調査するには長い時間がかかります。GC では、世代 1 のコレクションを行うときに常に世代 0 のコレクションも行い、世代 2 のコレクションを行うときはヒープ全体のコレクションを行います。このため、世代 2 のコレクションは "フル" コレクションと呼ばれます。さまざまなヒープ コレクションのパフォーマンスに関する詳細については、2009 年 10 月号の「CLR 徹底解剖」コラム (msdn.microsoft.com/magazine/ee309515) をご覧ください。

同時実行 GC

ガベージ コレクションを実行する単純なアルゴリズムは、GC が機能している間すべてのプログラム スレッドを一時停止する実行エンジンを用意することです。この種のコレクションを、"ブロッキング" コレクションと呼びます。このようなコレクションでは、GC は、ある世代から別の世代に移動したり、散在するメモリ セグメントをコンパクトにまとめたりするなど、固定されていないメモリを移動できます。こうした移動を、プログラムが認識することはありません。もしもプログラム スレッドの実行中にメモリを移動すれば、プログラムからはメモリが破損したように見えます。

ただし、ガベージ コレクションの機能の中にはメモリを変更しない機能もあります。CLR では、最初のバージョンから、ガベージ コレクションをプログラムと "同時実行" する GC モードを提供してきました。このようなコレクションでは、ガベージ コレクション中にプログラム スレッドを一時停止しないで、GC のほぼすべての機能が実行されます。

プログラムが認識している状態を変更しないで GC で実行できる機能は多数あります。たとえば、GC では、プログラムからアクセスできるすべてのメモリを検出できます。GC がヒープを調査している間も、プログラム スレッドは実行を継続できます。実際のコレクションを実行する前に GC で必要な作業は、メモリの調査中に変更された内容を検出することだけです。たとえば、プログラムによって新しいオブジェクトが割り当てられた場合は、そのオブジェクトをアクセス可能としてマークする必要があります。検出処理の最後に、GC では (同時実行されない GC と同様に) すべてのスレッドをブロックするよう実行エンジンに指示してから、この時点でアクセス可能なすべてのメモリのコレクションを完了します。

バックグラウンド GC

同時実行 GC はほとんどのシナリオで常に優れたパフォーマンスを発揮してきましたが、今回大幅に強化されたシナリオが 1 つあります。既に説明したように、メモリは最も若い世代または LOH に割り当てられます。世代 0 と世代 1 は 1 つのセグメントに配置されます。このセグメントには存続時間が短いオブジェクトが保持されるため、"一過性" セグメントと呼ぶことにします。一過性セグメントがいっぱいになると、オブジェクトを割り当てる場所がなくなるため、プログラムでは新しいオブジェクトを作成できなくなります。GC では一過性セグメントのガベージ コレクションを行って領域を解放し、割り当てを続行できるようにする必要があります。

同時実行 GC では、同時実行コレクションの実行中は、このような作業のいずれも実行できないことが問題になります。GC のスレッドでは、プログラム スレッドの実行中にメモリを移動できません。したがって、古いオブジェクトを世代 2 に移動できません。さらに、GC が既に実行中のため、一過性セグメントのガベージ コレクションを開始できません。しかし、プログラムを続行するには、GC が一過性セグメントのメモリを解放しなければなりません。非常に厄介な問題です。同時実行 GC がプログラムで認識している状態を変更していることが原因ではなく、プログラムでメモリを割り当てることができないことが原因でプログラム スレッドを停止する必要があります。同時実行コレクションで、アクセスできるすべてのメモリを検出した後で一過性セグメントがいっぱいになっていることが判明したら、すべてのスレッドを一時停止して、ブロッキング圧縮を実行します。

この問題に端を発して、バックグラウンド GC の開発が行われました。フル コレクションの機能の大部分が、常に、独自のスレッド上で、バックグラウンド実行されるという点では、バックグラウンド GC の動作は同時実行 GC と似ています。主な違いは、バックグラウンド GCでは、フル コレクションでデータを収集している最中でも、一過性セグメントのガベージ コレクションを実行できる点です。つまり、一過性セグメントがいっぱいになっても、プログラムを引き続き実行できます。GC が一過性セグメントのガベージ コレクションを実行するだけで、すべての処理が期待どおり実行されます。

プログラムの遅延に対するバックグラウンド GC の効果は絶大です。バックグラウンド GC を実行すると、プログラムの実行が中断される回数が大幅に減少し、中断された場合でも中断時間が短くなることが確認されています。

バックグラウンド GC は Silverlight 4 の既定のモードです。ただし、OS X には GC をバックグラウンド モードや同時実行モードで実行するのに必要な OS のサポートが一部不足しているため、Windows でのみ有効です。

NGen のパフォーマンス向上

C# や Visual Basic などのマネージ言語のコンパイラでは、ユーザーのコンピューターで実行できるコードは直接生成されず、MSIL という中間言語が生成され、プログラムの実行時に JIT コンパイラによって MSIL が実行可能コードにコンパイルされます。

MSIL を使用することには、セキュリティから移植性まで多種多様なメリットがありますが、JIT コンパイルされるコードには 2 つの代償が伴います。1 つ目の代償は、プログラムの Main 関数をコンパイルして実行する前に、.NET Framework コードをコンパイルしておく必要があることです。そのため、JIT コンパイルが完了するまで、ユーザーはプログラムの実行開始を待機しなければなりません。2 つ目の代償は、ユーザーのコンピューターで実行する Silverlight プログラムごとに、使用されるすべての .NET Framework コードをコンパイルする必要があることです。

この 2 つの問題に、NGen が役に立ちます。NGen は、.NET Framework コードをインストール時にコンパイルします。そのため、プログラムの実行開始時には既にコードがコンパイルされています。NGen でコンパイルされたコードは多くの場合複数のプログラムで共有できるため、複数の Silverlight プログラムを実行するときに、ユーザーのコンピューター上のワーキング セットが減少します。NGen によって起動時間が短縮され、ワーキング セットが向上するしくみの詳細については、2006 年 5 月号の「CLR 徹底解剖」コラム (msdn.microsoft.com/magazine/cc163610、英語) をご覧ください。

Silverlight プログラムの大半が .NET Framework のコードによって構成されているため、Silverlight 2 と Silverlight 3 で NGen を使用できなかったことは、起動時間に大きな影響を与えていました。あらゆるプログラムの起動パスに対してライブラリ コードを最適化してコンパイルするには、JIT コンパイラは時間がかかりすぎました。

Silverlight 2 と Silverlight 3 でマイクロソフトが採用した解決策は、JIT コンパイラによってコード生成の最適化を行わないことでした。依然としてコードをコンパイルすることは必要でしたが、JIT コンパイラでは単純なコードを生成するようになったため、コンパイルにそれほど長い時間がかからなくなりました。リッチ インターネット アプリケーションの Web シナリオ向けに記述されたほとんどのプログラムは、従来のデスクトップ アプリケーションに比べて小規模で実行時間がそれほど長くありません。さらに重要なことに、これらのプログラムはたいてい対話型プログラムなので、ほとんどの時間がユーザーの入力を待機することに費やされます。Silverlight 2 と Silverlight 3 で問題になったシナリオでは、最適化されたコードの生成よりも起動時間の短縮の方がずっと重要でした。

Silverlight Web アプリケーションの進化に合わせて、マイクロソフトはエクスペリエンスを向上し続けるための変更を加えています。たとえば、Silverlight 3 では、Silverlight アプリケーションをデスクトップにインストールして実行するサポートを追加しました。通常、このようなデスクトップ アプリケーションは、従来の Web シナリオに見られる小規模で対話型のアプリケーションに比べて、大規模で多くの機能を実行します。Silverlight 自体にも、Windows 7 でのタッチ入力のサポート、Bing Maps Web サイトで使用されている機能豊富な写真操作など、大量のコンピューター処理を実行する機能が多数追加されています。このようなシナリオではすべて、実行効率を上げるためにコードを最適化する必要があります。

Silverlight 4 では、起動時のパフォーマンス向上とコードの最適化が行われます。JIT コンパイラでは、デスクトップ .NET アプリケーションと同じ最適化が Silverlight でも行われるようになります。Silverlight の .NET Framework アセンブリに対して NGen が有効になったため、最適化の実行が可能になりました。Silverlight をインストールするときに、Silverlight ランタイムに含まれるマネージ コードがすべて自動的にコンパイルされ、ハード ディスクに保存されます。ユーザーが Silverlight プログラムを実行すると、.NET Framework コードのコンパイルを待たずにプログラムの実行が開始します。同様に重要な点として、プログラムの実行速度が向上するよう Silverlight プログラムのコードが最適化されるようになり、ユーザーのコンピューター上で実行している複数の Silverlight プログラム間で .NET Framework コードを共有できるようになりました。

Silverlight 4 では、インストール時に .NET Framework アセンブリのネイティブ イメージが作成されます。起動時のパフォーマンスだけがパフォーマンス上の問題になるアプリケーションもあります。たとえば、メモ帳について考えてみましょう。メモ帳では、すばやく起動することが重要ですが、ユーザーが入力を始めれば (入力速度が実行速度を上回らない限り) 実行速度は問題にならなくなります。このようなプログラムの場合、アプリケーションのスタートアップ コードを JIT コンパイルするのにかかる時間が原因で、パフォーマンスが低下することがあります。Silverlight 4 では、ほとんどのアプリケーションが 400 ~ 700 ミリ秒以内に起動され、実行時のパフォーマンスが最大 60% 向上します。

基本クラス ライブラリ (BCL) はマネージ API の中核となりますが、Silverlight 4 ではマネージ API が NGen によってサポートされるようになりました。そこで次に、BCL の新機能を紹介しましょう。

BCL の新機能

Silverlight 4 で新しく行われた BCL の機能強化の多くは .NET Framework 4 の新機能でもあり、.NET Framework 4 の新機能については既に説明しました。ここでは、Silverlight 4 に含まれる機能強化についてごく簡単に説明します。

コード コントラクトでは、Silverlight コードでの事前条件、事後条件、およびオブジェクト インバリアントを指定する組み込みの手段が提供されます。コード コントラクトを使用すると、コードでの前提条件をわかりやすく表現でき、早い段階でバグを発見するのに役立ちます。コード コントラクトを使用することには、他にも多くのメリットがあります。詳細については、2009 年 8 月号の Melitta Andersen による「CLR 徹底解剖」コラム (msdn.microsoft.com/magazine/ee236408)、コード コントラクトの開発者ラボ サイト (msdn.microsoft.com/devlabs/dd491992、英語)、および BCL チームのブログ (blogs.msdn.com/bclteam、英語) をご覧ください。

組は、メソッドから複数の値を返す際に最もよく使用されます。F# などの関数型言語や IronPython などの動的言語でよく使用されますが、Visual Basic や C# で使用するのも簡単です。組の設計の詳細については、2009 年 7 月号の Matt Ellis による「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 と String.Join のオーバーロードが IEnumerable<T> パラメーターを受け取るようになりました。これらの String.Concat と String.Join の新しいオーバーロードを使用すると、コレクションを配列に変換しておかなくても、IEnumerable<T> を実装する任意のコレクションを連結できます。

Stream.CopyTo メソッドを使用すると、1 行のコードで、あるストリームから読み取った内容を別のストリームに簡単に書き込めます。

ここに挙げた新機能以外にも、分離ストレージの機能が一部強化され、信頼済みの Silverlight アプリケーションから System.IO を使用してファイル システムの一部に直接アクセスできるようになります。

分離ストレージの機能強化

分離ストレージは、Silverlight アプリケーションがクライアントにデータを保存するのに使用する仮想ファイル システムです。Silverlight の分離ストレージの詳細については、2009 年 3 月号の「CLR 徹底解剖」コラム (https://msdn.microsoft.com/en-us/magazine/dd458794) をご覧ください。

Silverlight 4 での分離ストレージの特筆すべき機能強化は、パフォーマンス面での機能強化です。Silverlight 2 のリリース以来、分離ストレージのパフォーマンスに関して、開発者から多数のフィードバックがマイクロソフトに寄せられました。Silverlight 3 では、分離ストレージからデータを読み取るパフォーマンスを大幅に向上する変更を行いました。Silverlight 4 ではさらに一歩踏み込んで、分離ストレージにデータを書き込む際に開発者が経験していたパフォーマンスのボトルネックに対処しています。Silverlight 4 では、全体として、分離ストレージのパフォーマンスが大きく向上しました。

また、分離ストレージ内のファイル名の変更やファイルのコピーに簡単な方法がないという意見も寄せられました。ファイル名を変更するには、元のファイルを手動で読み取って、新しいファイルを作成して書き込んでから、元のファイルを削除しなければなりませんでした。同様の方法でディレクトリ名も変更できましたが、さらに多くのコード行が必要で、名前を変更するディレクトリにサブディレクトリが含まれていると特に多くのコード行が必要でした。この方法でも機能はしますが、必要以上に多くのコードを記述することになり、ディスク上のファイルやディレクトリの名前を変更するよう OS に指示するだけという方法に比べれば、効率的ではありませんでした。

Silverlight 4 では、CopyFile、MoveFile、および MoveDirectory という新しいメソッドが IsolatedStorageFile クラスに追加されています。これらのメソッドを呼び出せば、上記の操作を効率よく実行できます。また、分離ストレージ内のファイルやディレクトリに関する詳細情報を返す、GetCreationTime、GetLastAccessTime、および GetLastWriteTime という新しいメソッドも追加されています。

Silverlight 4 で追加されたもう 1 つの新しい API は、IsolatedStorageFile.IsEnabled プロパティです。以前は、分離ストレージが有効かどうか判断する方法は、分離ストレージを使用してみて IsolatedStorageException (分離ストレージが無効な場合にスローされる例外) をキャッチすることだけでした。新しい IsEnabled 静的プロパティを使用すると、分離ストレージが有効かどうかを簡単に判断できます。

現在、Internet Explorer、Firefox、Chrome、Safari などの多くのブラウザーでは、履歴、Cookie、およびその他のデータが保存されないプライベート ブラウズ モードがサポートされています。Silverlight 4 では、プライベート ブラウズ設定を尊重して、ブラウザーがプライベート モードの場合はアプリケーションが分離ストレージにアクセスしてローカル コンピューターに情報を保存しないようにします。このような状況では、IsEnabled プロパティは false を返し、分離ストレージを使用しようとすると IsolatedStorageException がスローされます。これは、ユーザーが分離ストレージを明示的に無効にした場合と同じ動作です。

ファイル システムへのアクセス

Silverlight アプリケーションは、部分的に信頼されるセキュリティ サンドボックス内で実行されます。セキュリティ サンドボックスではローカル コンピューターへのアクセスを制限し、アプリケーションに多数の制約を課して、悪意のあるコードが害を及ぼさないようにします。たとえば、部分的に信頼される Silverlight アプリケーションからファイル システムに直接アクセスすることはできません。アプリケーションからクライアントにデータを保存する必要があれば、分離ストレージ内にデータを保存するほかありません。より広範なファイル システムにアクセスする方法は、OpenFileDialog クラスまたは SaveFileDialog クラスを使用する場合だけです。

Silverlight 3 では、アプリケーションをブラウザー外にインストールして実行する機能が追加されました。この機能によっていくつかの興味深いオフライン シナリオが可能になりましたが、このようなアプリケーションもブラウザー内で実行しているアプリケーションと同じようにサンドボックス内で実行されます。Silverlight 4 では、ブラウザー外アプリケーションが権限を昇格して実行されるように、アプリケーション自体を構成できます。このような信頼済みのアプリケーションでは、インストール後にサンドボックスの制約の一部を迂回できます。たとえば、信頼済みのアプリケーションでは、ユーザー ファイルにアクセスでき、クロスドメイン アクセスの制限を受けずにネットワークを使用でき、ユーザーの同意や開始の要件を迂回でき、ネイティブな OS の機能にアクセスできます。

権限の昇格が必要なブラウザー外実行アプリケーションをユーザーがインストールするときに、通常のインストール プロンプトが警告に置き換わり、このアプリケーションからはユーザーのデータにアクセスでき、信頼済みの Web サイトからのみインストールすべきであることが通知されます。

信頼済みのアプリケーションでは、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 では /) が 2 つのパスの間に挿入されます。File.WriteAllText メソッドは、ファイルを作成 (既存のファイルがある場合はそのファイルを上書き) して、"Hello World!" というテキストをファイルに書き込みます。

パフォーマンスの向上と機能の追加

ここまで説明したように、Silverlight 4 の新しい CLR では、ランタイムと基本クラスの両方が強化されています。新しい GC の動作、Silverlight Framework アセンブリで NGen が実行されるようになったこと、および分離ストレージのパフォーマンスが向上したことで、アプリケーションの起動時間が短縮され、Silverlight 4 での実行が強化されます。BCL の機能強化によって、アプリケーションでは少ないコードで多くの処理を実行できるようになり、信頼済みのアプリケーションからファイル システムにアクセスできるようになったことから、まったく新しいシナリオがサポートされるようになります。

Andrew Pardoe は、マイクロソフトで CLR のプログラム マネージャーを務めています。彼は、デスクトップおよび Silverlight ランタイム両方の実行エンジン全般に携わっています。連絡先は andrew.pardoe@microsoft.com (英語のみ) です。

Justin Van Patten は、マイクロソフトの CLR チームのプログラム マネージャーとして基本クラス ライブラリを担当しています。Justin には BCL チームのブログ (https://blogs.msdn.com/b/bclteam/、英語) から連絡を取ることができます。

この記事のレビューに協力してくれた技術スタッフの Surupa Biswas、Vance Morrison、および Maoni Stephens に心より感謝いたします。