.NET アプリケーションのパフォーマンスとスケーラビリティの向上
J.D. Meier, Srinath Vasireddy, Ashish Babbar, and Alex Mackman
Microsoft Corporation
May 2004
日本語版最終更新日 2005 年 11 月 27 日
要約: この How To 情報では、シリアル化パフォーマンスを向上させる方法を示します。ここでは、オブジェクトのマーシャリングのために Web サービスが使用する XmlSerializer クラス、および Microsoft(R) .NET リモート処理が使用する SoapFormatter クラスと BinaryFormatter クラスを扱います。また、パフォーマンスに関する一般的な推奨事項に加え、DataSet シリアル化パフォーマンスを向上させるためのアプローチを、重点的に取り上げます。
対象製品
- .NET Framework version 1.1
目次
概要
理解しておくべきこと
シリアル化パフォーマンスの向上
DataSet シリアル化パフォーマンスの向上
Web サービスにかかわるシリアル化に関する考慮事項
リモート処理にかかわるシリアル化に関する考慮事項
補足文書
概要
シリアル化は、オブジェクトの状態情報を保持することにより、そのオブジェクトを格納し、後に再生成することが可能となるように用いられます。ASP.NET は、シリアル化によってオブジェクトをセッション状態に格納します。また、シリアル化は、オブジェクトをアプリケーション ドメイン、プロセス、あるいはコンピュータなどのリモート処理境界を越えて送る場合にも使用されます。さらに、Web サービスとの間で引数を受け渡しする場合にも用いられます。
.NET Framework は、2 つのシリアル化メカニズムを提供します。
- ASP.NET Web サービスは、XmlSerializer クラスを使用してシリアル化を実行します。
- .NET 処理は、IFormatter を実装する 2 つのクラス (BinaryFormatter と SoapFormatter) を使用します。フォーマッタ オブジェクトによるシリアル化をサポートするためには、型に Serializable 属性を定義します。
シリアル化は頻繁に利用されるため、シリアル化パフォーマンスは .NET アプリケーションにおいて十分に考慮すべきものです。パフォーマンスの向上に用いることのできる技法は、数多くあります。この How To 情報では、これらの技法を紹介していきます。
理解しておくべきこと
シリアル化を用いる予定なら、以下について理解しておく必要があります。
- クライアント-サーバー間のデータ コントラクトを考慮し、リモート アクセスの効率性に配慮してインターフェイスを設計するようにしてください。例えば、チャッティーな (処理規模が小さく対話の多い) インターフェイスは避けてください。必要なら、ファサード パターンを実装し、既存のチャッティー インターフェイスをラップしてラウンド トリップ数を減らしてください。
- Web サービスで使われる XmlSerializer は、クラスの public フィールドと public プロパティを共にシリアル化します。
- .NET リモート処理で使われる BinaryFormatter クラスと SoapFormatter クラスは、リモートのメソッド呼び出しに対してオブジェクトを値渡しする場合は常に、private の表示が有るものも含め、クラスのフィールドをすべてシリアル化することを要求します。
-
XmlSerializer はプライベート データをシリアル化しないため、BinaryFormatter や SoapFormatter よりも素早く DataSet オブジェクトをシリアル化します。DataSet オブジェクトは内部プロパティのコレクションを保持し、シリアル化が高くつく場合がある、DataView や XML Diffgram などの機能を提供します。
- パブリック コンストラクタ、および少なくとも 1 つのシリアル化可能なパブリック メンバを備え、宣言的なセキュリティを備えていないあらゆる型は、XmlSerializer クラスによってシリアル化可能です。Hashtable など、XmlSerializer によって処理できないメンバ変数を含む型は、シリアル化されません。
-
BinaryFormatter は、SoapFormatter の場合よりもコンパクトなバイト ストリームを生成します。SoapFormatter は通常、異種プラットフォーム間の相互運用に用いられます。
-
Serializable 属性を使用している場合、.NET ランタイムにおけるシリアル化では、リフレクションを使ってシリアル化すべきデータを特定します。public フィールド、private フィールド、protected フィールド、internal フィールドを含め、すべての非一時フィールドはシリアル化されます。XML シリアル化では、リフレクションを使ってシリアル化実行のための特別クラスを生成します。
-
ISerializable インターフェイスを使用すれば、データのシリアル化方法を明示的にコントロールできます。
- バイナリ シリアル化は通常、XML シリアル化に比べて出力がコンパクトなため、パフォーマンス面で優れています。
- XML シリアル化において、IDictionary を実装する HashTable や ListDictionary などのクラスをシリアル化することはできません。IDictionary を実装するオブジェクトをシリアル化する必要があるなら、独自のシリアル化機能を実装しなければなりません。
- 機密性の高いデータについては、この How To 情報で後述する「NonSerialized 属性または XmlIgnore 属性の使用」で示すように、機密性の高いフィールドに NonSerialized 属性または XmlIgnore 属性を付けることによってシリアル化するのは、避けるべきです。
シリアル化パフォーマンスの向上
ランタイムのシリアル化 パフォーマンスを向上させる方法には、さまざまなものがあります。例えば、クラス内の特定フィールドを無視するようにランタイム シリアライザに指示することにより、シリアル化データ ストリームのサイズを小さくすることができます。また、ISerializable インターフェイスを実装し、シリアル化 (または逆シリアル化) プロセスを明示的にコントロールして、パフォーマンス向上を図ることも可能です。
NonSerialized 属性または XmlIgnore 属性の使用
属性を使用することにより、クラス内の特定フィールドがシリアル化されないようにすることができます。これにより、出力ストリームのサイズは小さくなり、シリアル化処理オーバーヘッドも軽減されます。また、このアプローチは、機密性の高いデータのシリアル化を防ぐ上でも効果的です。
属性には、NonSerialized と XmlIgnore の 2 つがあります。使用すべき属性は、用いているシリアライザによって決まります。
- .NET リモート処理で使用される SoapFormatter クラスおよび BinaryFormatter クラスは、NonSerialized 属性を認識します。
- Web サービスで使用される XmlSerializer クラスは、XmlIgnore 属性を使用します。
以下のコードでは、XmlIgnore 属性を使用しています。
[Serializable]
public class Employee
{
public string FirstName;
[XmlIgnore]
public string MiddleName;
public string LastName;
}
明示的コントロールのための ISerializable の使用
ISerializable インターフェイスを使用すれば、データのシリアル化方法を明示的にコントロールできます。ただし、このインターフェイスの実装は、最終手段とするべきです。このアプローチを用いた場合、.NET Framework の今後のバージョンが提供する新しいフォーマッタや、シリアル化のもたらすフレームワーク改良によるメリットは、受けられなくなります
メモ: 一般的に、ISerializable の実装は、以下の理由で避けるべきです。
- 派生クラスが、シリアル化のために ISerializable を実装しなければならなくなる。
- コンストラクタと GetObjectData をオーバーライドしなければならなくなる。
- 型について、将来的な機能およびパフォーマンスの改善によるメリットを受ける上で、制限される。
ISerializable の実装
ISerializable インターフェイスは、GetObjectData という、シリアル化すべきデータを厳密に特定するために使用する、単一のメソッドを含んでいます。
public interface ISerializable
{
public void GetObjectData(SerializationInfo info, StreamingContext context);
}
以下のコードは、GetObjectData メソッドの単一実装を示すものです。データは、現在のオブジェクト インスタンスから取得し、SerializationInfo オブジェクトに格納します。
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("id", ID);
info.AddValue("firstName", firstName);
...
info.AddValue("zip", zip);
}
ISerializable を実装する際には、SerializationInfo 引数および StreamingContext 引数を受け入れる新しいコンストラクタも生成しなければなりません。このコンストラクタは、.NET ランタイムによって呼び出され、オブジェクトを逆シリアル化します。コンストラクタでは、提供された SerializationInfo オブジェクトからデータを読み取り、現在のオブジェクト インスタンスに格納します。これを、以下のコードで示します。
[Serializable]
public class CustomerInterface : ISerializable
{
protected CustomerInterface(SerializationInfo info, StreamingContext context)
{
ID = info.GetInt32("id");
firstName = info.GetString("firstName");
...
zip = info.GetString("zip");
}
...
}
基本クラス メンバのシリアル化
ISerializable を実装している場合は、必ず基本クラス メンバをシリアル化してください。基本クラスも ISerializable を実装しているなら、基本クラスの GetObjectData を呼び出すことができます。基本クラスが ISerializable を実装していないなら、各要求値を格納する必要があります。
バージョン管理に関する考慮事項
前にシリアル化したクラスのメンバ変数を追加、除去、あるいは名称変更した場合、保持し続けている既存のオブジェクトは、正常に逆シリアル化できなくなります。これは、ISerializable を実装せず、GetValue を呼び出しているだけのクラスについて、特に言えることです。この場合、要求値がシリアル化ストリームの中になければ、例外が発生します。
この問題を解決するには、SerializationInfoEnumerator を使用し、SerializationInfo オブジェクト内のアイテムについて検索を行った後、スイッチを用いて値をセットしてください。このアプローチにより、シリアル化ストリーム内に存在するフィールドのみを修復し、消失したフィールドを手動で初期化することが可能となります。
DataSet シリアル化パフォーマンスの向上
多くのアプリケーションは、リモート層の間で DataSet オブジェクトの受け渡しを行います。しかし、大きなシリアル化コストを発生させるこのアプローチは、アプリケーションのパフォーマンス目標の達成を妨げかねません。
DataSet は、子オブジェクト階層を伴う、複雑なオブジェクトです。そのため、DataSet のシリアル化は、プロセッサに大きな負荷をかけます。また、DataSet オブジェクトは、バイナリ フォーマッタを使っている場合でも、XML としてシリアル化されます。従って、出力ストリームは大きくなります。
DataSet シリアル化パフォーマンスの向上に用いることのできる技法は、数多くあります。
列の別名指定
長い列名に対して短い別名を指定し、シリアル化データのサイズを小さくすることができます。SQLで as キーワードを使用して列名の別名を指定する方法を、以下に示します。
DataSet objDataset = new DataSet("Customers");
SqlDataAdapter myAdapter = new SqlDataAdapter("Select CustomerId as C,CompanyName
as D,ContactName as E,ContactTitle as F from Customers",myConnection);
myAdapter.Fill(objDataset);
Stream serializationStream = new
MemoryStream(byteData,0,byteData.Length,true,true);
serializationStream.Position=0;
iBinForm.Serialize(serializationStream,objDataset);
同一データの複数バージョンに対するシリアル化の回避
DataSet のデータを変更すると同時に、そのデータについて複数のコピーが保持されることになります。DataSet は、変更値と共に、オリジナル データを保持します。新旧両方の値をシリアル化する必要がないなら、DataSet をシリアル化する前に AcceptChanges を呼び出し、内部バッファをリセットしてください。DataSet セットに含まれているデータの量と変更回数によっては、このアプローチによってシリアルするデータの量を大幅に減らすことできます。これを、以下のコード例で示します。
// データセットにデータを入れる
customers.Fill(northwind, "Customers");
orders.Fill(northwind, "Orders");
// ... データを変更
northwind.AcceptChanges();
// 変更を受け入れ、内部バッファをリセット
// ... データセットをシリアル化
シリアル化する DataTables 数の削減
DataSet に含まれる DataTables をすべて送る必要がないなら、送信する DataTables を別の DataSet にコピーすることを検討してください。これにより、処理する DataTables 数が減り、DataView によって使われる変更バッファが初期化されるため、シリアル化するデータの量を減らすことができます。
customers.Fill(northwind, "Customers");
orders.Fill(northwind, "Orders");
//... データを使用または変更
DataSet subset = new DataSet();
// 顧客 DataTable のみをコピー
subset.Tables.Add( northwind.Tables["customers"].Copy());
// ... DataSet の一部をシリアル化
バイナリ シリアル化のための DataSet のオーバーライド
デフォルトでは、BinaryFormatter を使っている場合でも、DataSet は XML としてシリアル化されます。このため、シリアル化データ ストリームは大きくなります。出力フォーマットをコンパクトにするために、DataSet クラスをオーバーライドし、独自のシリアル化を実装することを検討してください。
Web サービスにかかわるシリアル化に関する考慮事項
Web サービスとの間で受け渡しするシリアル化データのサイズを小さくするためにデータ ストリームを圧縮する技法には、さまざまなものがあります。また、XmlSerializer クラスの効率的な初期化や XmlIgnore の使用により、その他の最適化を実現できます。以下のアプローチについて、考慮してください。
- シリアル化データの圧縮
- FromTypes の起動時呼び出しによる XmlSerializer の初期化
- XmlIgnore 属性の使用
シリアル化データの圧縮
Web サービスとの間で受け渡しするシリアル化データを圧縮する技法には、さまざまなものがあります。
- サーバー側とクライアント側の両方に SoapExtensions を実装し、データの圧縮と解凍をする。
-
HttpModule を実装し、gzip 圧縮などで応答を圧縮した後、プロキシのクライアントでデータを解凍する。これを実行するためには、以下に示すように、Web サーバー クライアント プロキシで GetWebRequest メソッドと GetWebResponse メソッドをオーバーライドする必要があります。
// Web サービス プロキシで GetWebRequest メソッドをオーバーライド
protected override WebRequest GetWebRequest(Uri uri)
{
WebRequest request = base.GetWebRequest(uri);
request.Headers.Add("Accept-Encoding", "gzip, deflate");
return request;
}
// Web サービス プロキシで GetWebResponse メソッドをオーバーライド
protected override WebResponse GetWebResponse(WebRequest request)
{
// Web サービスからの応答を解凍
return response;
}
- インターネット インフォメーション サービス (IIS) 5.0 の HTTP 圧縮機能を使った後、IIS 5.0 圧縮に準拠したユーティリティを使用し、クライアント側プロキシ内で応答を圧縮する。やはり、Web サービス クライアント プロキシで GetWebRequest メソッドと GetWebResponse メソッドをオーバーライドする必要があります。
FromTypes の起動時呼び出しによる XmlSerializer の初期化
XmlSerializer は、ある型を最初に見つけるとシリアル化を実行するためのコードを生成し、そのコードを以後の利用のためにキャッシュします。一方、XmlSerializer で FromTypes 静的メソッドを呼び出すと、XmlSerializer はシリアル化しようとしている型について必要なコードを速やかに生成し、キャッシュします。このアプローチにより、特定の型を最初にシリアル化するために要する時間を、短縮できます。これを、以下のコード例で示します。
static void OnApplicationStart()
{
Type[] myTypes = new Type[] { Type.GetType("customer"), Type.GetType("order") };
XmlSerializer.FromTypes( myTypes );
}
XmlIgnore 属性の使用
前述したように、シリアル化する必要のないフィールドが出力ストリームに含まれないようにするために、XmlIgnore 属性の使用を検討することができます。
リモート処理にかかわるシリアル化に関する考慮事項
.NET リモート処理のインフラストラクチャでは、シリアル化実行のために IFormatter インターフェイスを実装するフォーマッタが使われます。.NET Framework は、SoapFormatter と BinaryFormatter という、2 つのフォーマッタを提供します。ただし、独自フォーマッタを実装することも可能です。.NET リモート処理を利用する場合、private フィールド、public フィールド、protected フィールド、internal フィールドを含め、すべての非一時フィールドがシリアル化されます。
NonSerialized 属性の使用
パフォーマンスとセキュリティを最適化するためには、前述したように NonSerialized 属性を使用し、不要なフィールドや機密性の高いフィールドがシリアル化されないようにしてください。
DataSet とリモート処理
アプリケーションが DataSet を使用していて、シリアル化パフォーマンスに関する問題が発生した場合は、シリアル化ラッパー クラスを実装することを検討してください。このアプローチにより、リモート処理によって通常発生する、一時メモリ割り当てを軽減することができます。この問題についての詳細およびサンプルは、Microsoft サポート技術情報の Knowledge Base 文書 829740 「データセットのシリアル化とリモート処理のパフォーマンスの向上」 を参照してください。
補足文書
詳細は、以下を参照してください。