.NET アプリケーションのパフォーマンス関連のヒントとトリック

 

エマニュエル・シャンツァー
Microsoft Corporation

2001 年 8 月

概要: この記事は、マネージド 環境で最適なパフォーマンスを得るためにアプリケーションを調整する開発者向けです。 サンプル コード、説明、および設計ガイドラインは、データベース、Windows フォーム、ASP アプリケーション、および Microsoft Visual Basic および Managed C++ の言語固有のヒントに対応しています。 (25ページ印刷)

内容

概要
すべてのアプリケーションのパフォーマンスに関するヒント
データベース アクセスに関するヒント
ASP.NET アプリケーションのパフォーマンスに関するヒント
Visual Basic での移植と開発に関するヒント
マネージド C++ での移植と開発に関するヒント
その他のリソース
付録: 仮想呼び出しと割り当てのコスト

概要

このホワイト ペーパーは、.NET 用のアプリケーションを作成し、パフォーマンスを向上させるためのさまざまな方法を探している開発者向けのリファレンスとして設計されています。 .NET を初めて使用する開発者は、プラットフォームと選択した言語の両方について理解している必要があります。 この論文は厳密にその知識に基づいており、プログラマはプログラムを実行するのに十分な知識を既に持っていることを前提としています。 既存のアプリケーションを .NET に移植する場合は、ポートを開始する前に、このドキュメントを読む価値があります。 ここでのヒントの一部は、設計フェーズで役立ち、ポートを開始する前に注意する必要がある情報を提供します。

このペーパーは、プロジェクトと開発者の種類別に整理されたヒントを含むセグメントに分かれています。 最初の一連のヒントは、任意の言語で記述するための必読であり、共通言語ランタイム (CLR) 上の任意のターゲット言語に役立つアドバイスが含まれています。 関連するセクションの後に、ASP 固有のヒントが示されます。 2 番目のヒントセットは言語別に整理されており、Managed C++ と Microsoft® Visual Basic の使用に関する特定のヒントを扱います®。

スケジュールの制限により、バージョン 1 (v1) の実行時には、最初に最も広範な機能をターゲットにしてから、後で特殊なケースの最適化を処理する必要がありました。 その結果、パフォーマンスが問題になるいくつかのピジョンホールケースが発生します。 そのため、このケースを回避するために設計されたいくつかのヒントについて説明します。 これらのヒントは、次のバージョン (vNext) では関連しません。これらのケースは体系的に識別され、最適化されるためです。 私たちが行くにつれて彼らを指摘します、そして、それが努力する価値があるかどうかを決めるのはあなた次第です。

すべてのアプリケーションのパフォーマンスに関するヒント

任意の言語で CLR に取り組む際に覚えておく必要があるヒントがいくつかあります。 これらはすべてのユーザーに関連しており、パフォーマンスの問題に対処する際の防御の最初の行である必要があります。

スローする例外の数を減らします

例外をスローすると非常にコストがかかる可能性があるため、多くの例外をスローしないようにしてください。 Perfmon を使用して、アプリケーションがスローしている例外の数を確認します。 アプリケーションの特定の領域で、予想よりも多くの例外がスローされることに驚くかもしれません。 細分性を高めるには、パフォーマンス カウンターを使用してプログラムで例外番号をチェックすることもできます。

例外が多いコードを見つけて設計すると、適切なパフォーマンスが得られます。 これは try/catch ブロックとは関係ありません。 実際の例外がスローされた場合にのみコストが発生します。 try/catch ブロックは、必要な数だけ使用できます。 例外を不当に使用すると、パフォーマンスが低下します。 たとえば、制御フローに例外を使用するなどの点から離れる必要があります。

コストの高い例外の簡単な例を次に示します。 For ループを実行し、数千個または数千個の例外を生成してから終了します。 速度の違いを確認するには、throw ステートメントをコメントアウトしてみてください。これらの例外により、大きなオーバーヘッドが発生します。

public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • 注意! 実行時には、例外を単独でスローできます。 たとえば、 Response.Redirect() はThreadAbort 例外をスローします。 例外を明示的にスローしない場合でも、 を実行する関数を使用できます。 Perfmon をチェックして実際のストーリーを取得し、デバッガーでソースをチェックしてください。
  • Visual Basic 開発者向け: Visual Basic では、オーバーフローや 0 除算などの例外がスローされるように、既定で int チェックをオンにします。 これをオフにしてパフォーマンスを向上させることができます。
  • COM を使用する場合は、HRESULTS が例外として返される可能性があることに注意してください。 これらを注意深く追跡してください。

チャンキーな呼び出しを行う

チャンキー呼び出しは、オブジェクトのいくつかのフィールドを初期化するメソッドなど、いくつかのタスクを実行する関数呼び出しです。 これは、非常に単純なタスクを実行し、複数の呼び出しを必要とするチャット呼び出し (異なる呼び出しでオブジェクトのすべてのフィールドを設定するなど) に対して表示されます。 単純な AppDomain メソッド呼び出しよりもオーバーヘッドが高いメソッド間で、チャット型の呼び出しではなく、チャンキーな呼び出しを行う必要があります。 P/Invoke、相互運用、リモート処理の呼び出しはすべてオーバーヘッドを発生させ、慎重に使用する必要があります。 これらの各ケースでは、アプリケーションを設計して、非常に多くのオーバーヘッドを発生させる小さな頻繁な呼び出しに依存しないようにする必要があります。

遷移は、マネージド コードがアンマネージド コードから呼び出されるたびに発生し、その逆も発生します。 ランタイムにより、プログラマは相互運用を非常に簡単に行うことができますが、これはパフォーマンスの価格で行われます。 移行が発生した場合は、次の手順を実行する必要があります。

  • データ マーシャリングを実行する
  • 呼び出し規則の修正
  • 呼び出し先が保存したレジスタを保護する
  • GC がアンマネージド スレッドをブロックしないようにスレッド モードを切り替える
  • マネージド コードへの呼び出しで例外処理フレームを作成する
  • スレッドを制御する (省略可能)

切り替え時間を短縮するには、可能な限り P/Invoke を使用してみてください。 オーバーヘッドは、31 命令に加えて、データ マーシャリングが必要な場合はマーシャリングのコストに加え、それ以外の場合は 8 回だけです。 COM 相互運用は、65 以上の命令を取って、はるかに高価です。

データ マーシャリングは常にコストがかかるとは限りません。 プリミティブ型はマーシャリングをまったく必要とせず、明示的なレイアウトを持つクラスも安価です。 実際の速度低下は、ASCI から Unicode へのテキスト変換など、データ変換中に発生します。 マネージド境界を越えて渡されるデータは、必要な場合にのみ変換されるようにしてください。プログラム全体で特定のデータ型または形式に同意するだけで、多くのマーシャリングオーバーヘッドを削減できます。

次の型は blittable と呼ばれます。つまり、sbyte、byte、short、ushort、int、uint、long、ulong、float、double のマーシャリングなしで、マネージド/アンマネージド境界を直接コピーできます。 これらは無料で渡すことができます。また、ValueTypes や blittable 型を含む 1 次元配列も渡すことができます。 マーシャリングの詳細については、MSDN ライブラリを参照してください。 多くの時間をマーシャリングに費やす場合は、慎重に読むことをお勧めします。

ValueTypes を使用したデザイン

可能な場合は単純な構造体を使用し、ボックス化やボックス化解除をあまり行わない場合は使用します。 速度の違いを示す簡単な例を次に示します。

using System;

namespace ConsoleApplication{

  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

この例を実行すると、構造体ループの方が桁違いに高速であることがわかります。 ただし、ValueTypes をオブジェクトのように扱うときは、使用に注意することが重要です。 これにより、プログラムに余分なボックス化とボックス化解除のオーバーヘッドが追加され、オブジェクトでスタックしていた場合よりもコストがかかる可能性があります。 これを実際に確認するには、上記のコードを変更して、foos と bar の配列を使用します。 パフォーマンスが多かれ少なかれ等しいことがわかります。

トレードオフ ValueTypes は Objects よりもはるかに柔軟性が低く、誤って使用するとパフォーマンスが低下します。 いつ、どのように使用するかについて十分に注意する必要があります。

上記のサンプルを変更し、配列またはハッシュテーブル内に foo と bar を格納してみてください。 1 回のボックス化とボックス化解除操作だけで、速度のゲインが消えることがわかります。

GC の割り当てとコレクションを調べることで、ボックス化とボックス化解除の頻度を追跡できます。 これは、Perfmon 外部またはコードのパフォーマンス カウンターを使用して行うことができます。

.NET Frameworkの「Run-Time テクノロジのパフォーマンスに関する考慮事項」の ValueTypes の詳細な説明を参照してください。

AddRange を使用してグループを追加する

コレクション内の各項目を反復的に追加するのではなく、 AddRange を使用してコレクション全体を追加します。 ほぼすべてのウィンドウ コントロールとコレクションに Add メソッドと AddRange メソッドの両方があり、それぞれ異なる目的に合わせて最適化されています。 Add は 1 つの項目を追加する場合に便利ですが、 AddRange には余分なオーバーヘッドがありますが、複数の項目を追加すると優先されます。 AddAddRange をサポートするクラスのほんの一部を次に示します。

  • StringCollection、TraceCollection など。
  • HttpWebRequest
  • UserControl
  • ColumnHeader

作業セットをトリミングする

ワーキング セットを小さく保つために使用するアセンブリの数を最小限に抑えます。 1 つの方法を使用するためだけにアセンブリ全体を読み込む場合は、非常に少ない利益のために多大なコストを支払います。 既に読み込んだコードを使用して、そのメソッドの機能を複製できるかどうかを確認します。

作業セットを追跡することは困難であり、おそらく論文全体の主題になる可能性があります。 役立つヒントを次に示します。

  • vadump.exeを使用して作業セットを追跡します。 これについては、管理環境のさまざまなツールに関する別のホワイト ペーパーで説明します。
  • パフォーマンス カウンターまたはパフォーマンス カウンターを参照してください。 読み込むクラスの数や、JITed を取得するメソッドの数に関する詳細なフィードバックを提供できます。 ローダーに費やした時間、またはページングに費やされた実行時間の割合を読み取ることができます。

文字列イテレーションに For ループを使用する - バージョン 1

C# では、foreach キーワード (keyword)を使用すると、リストや文字列などの項目をウォークして、各項目に対して操作を実行できます。 これは、多くの型に対して汎用列挙子として機能するため、非常に強力なツールです。 この一般化のトレードオフは速度であり、文字列の反復に大きく依存する場合は、代わりに For ループを使用する必要があります。 文字列は単純な文字配列であるため、他の構造体よりもはるかに少ないオーバーヘッドを使用してウォークできます。 JIT は(多くの場合)Forループ内の境界チェックやその他のものを最適化するのに十分なスマートですが、foreachウォークではこれを行うことは禁止されています。 最終的な結果は、バージョン 1 では、文字列の For ループが foreach を使用するよりも最大 5 倍高速になります。 これは将来のバージョンで変更されますが、バージョン 1 の場合、これはパフォーマンスを向上させる明確な方法です。

速度の違いを示す簡単なテスト方法を次に示します。 それを実行してから、 For ループを削除し、 foreach ステートメントのコメントを解除してみてください。 私のマシンでは、 For ループに約1秒かかり、 foreach ステートメントに約3秒かかりました。

public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

トレードオフForeach ははるかに読みやすくなり、将来的には、文字列などの特殊なケースの For ループと同じくらい高速になります。 文字列操作が実際のパフォーマンスの低下でない限り、少し混乱したコードは価値がないかもしれません。

文字列の複雑な操作に StringBuilder を使用する

文字列が変更されると、実行時に新しい文字列が作成されて返され、元の文字列はガベージ コレクトされます。 ほとんどの場合、これは高速で簡単な方法ですが、文字列が繰り返し変更されると、パフォーマンスに負担がかかり始めます。これらの割り当てはすべて最終的に高価になります。 文字列に 50,000 回を追加し、その後に StringBuilder オブジェクトを使用して文字列を変更するプログラムの簡単な例を次に示します。 StringBuilder コードははるかに高速であり、実行するとすぐにわかります。

namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}
namespace ConsoleApplication1.Feedback{
  using System;
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}

Perfmon を見て、何千もの文字列を割り当てずに保存される時間を確認してみてください。 .NET CLR メモリの一覧の下にある "% time in GC" カウンターを確認します。 また、保存した割り当ての数とコレクション統計を追跡することもできます。

トレードオフ=StringBuilder オブジェクトの作成には、時間とメモリの両方でオーバーヘッドが発生します。 高速メモリを搭載したマシンでは、約 5 つの操作を行っている場合、 StringBuilder は価値があります。 経験則として、私は10以上の文字列操作が、遅いマシンであっても、あらゆるマシンのオーバーヘッドの正当な理由であると言うでしょう。

Windows フォーム アプリケーションのプリコンパイル

メソッドは、最初に使用されるときに JITed されます。つまり、アプリケーションが起動時に多くのメソッド呼び出しを行う場合、より大きなスタートアップペナルティを支払います。 Windows フォームは OS で多数の共有ライブラリを使用します。また、それらを起動する際のオーバーヘッドは、他の種類のアプリケーションよりもはるかに高くなる可能性があります。 常にとは限りませんが、Windows フォームアプリケーションをプリコンパイルすると、通常、パフォーマンスが向上します。 他のシナリオでは、通常は JIT で処理を行うのが最善ですが、Windows フォーム開発者の場合は、見てみたいことがあります。

Microsoft では、 を呼び出 ngen.exeしてアプリケーションをプリコンパイルできます。 インストール時またはアプリケーションを配布する前に、ngen.exeを実行することを選択できます。 インストール時にngen.exeを実行するのが最も理にかなっています。これは、アプリケーションがインストールされているマシン用に最適化されていることを確認できるためです。 プログラムを出荷する前にngen.exeを実行する場合は、最適化をマシンで使用可能なものに制限 します 。 プリコンパイルがどれだけ役立つかを知るために、私は自分のマシンで非公式のテストを実行しました。 約 100 個のコントロールを持つ winforms アプリケーションである ShowFormComplex のコールド スタートアップ時間を次に示します。

コードの状態 Time
フレームワークの JITed

ShowFormComplex JITed

3.4 秒
フレームワークプリコンパイル済み、ShowFormComplex JITed 2.5 秒
フレームワークプリコンパイル済み、ShowFormComplex プリコンパイル済み 2.1 秒

各テストは再起動後に実行されました。 ご覧のように、Windows フォーム アプリケーションでは多くのメソッドが事前に使用されているため、プリコンパイルに大きなパフォーマンスが得られます。

ジャグ配列の使用 - バージョン 1

v1 JIT では、四角形の配列よりも効率的にジャグ配列 (単に "配列の配列") が最適化され、その違いは非常に顕著です。 C# と Visual Basic の両方で四角形の配列の代わりにジャグ配列を使用した結果のパフォーマンス向上を示す表を次に示します (数値が大きいほど優れています)。

  C# Visual Basic 7
割り当て (ジャグ)

割り当て (四角形)

14.16

8.37

12.24

8.62

ニューラル ネット (ジャグ)

ニューラル ネット (長方形)

4.48

3.00

4.58

3.13

数値並べ替え (ジャグ)

数値並べ替え (四角形)

4.88

2.05

5.07

2.06

割り当てベンチマークは単純な割り当てアルゴリズムであり、 ビジネス向けの定量的意思決定 に関するページ (Gordon、Pressman、Cohn;Prentice-Hall;が印刷されな ニューラル ネット テストでは、小さなニューラル ネットワーク上で一連のパターンが実行され、数値の並べ替えは自明です。 まとめると、これらのベンチマークは、実際のパフォーマンスを示す優れた指標です。

ご覧のように、ジャグ配列を使用すると、パフォーマンスがかなり大幅に向上する可能性があります。 ジャグ配列に対する最適化は、今後のバージョンの JIT に追加されますが、v1 の場合はジャグ配列を使用して時間を大幅に節約できます。

IO バッファー サイズを 4 KB から 8 KB の間に保持する

ほぼすべてのアプリケーションで、4 KB から 8 KB の間のバッファーによって最大のパフォーマンスが得られます。 非常に具体的なインスタンスの場合、より大きなバッファー (たとえば、予測可能なサイズの大きな画像を読み込む) から改善を得ることができますが、99.99% の場合、メモリのみが無駄になります。 BufferedStream から派生したすべてのバッファーを使用すると、任意のサイズに設定できますが、ほとんどの場合、4 と 8 は最適なパフォーマンスを実現します。

非同期 IO の機会を監視する

まれに、非同期 IO を利用できる場合があります。 一連のファイルをダウンロードして圧縮解除する例があります。あるストリームからビットを読み取り、CPU でデコードし、別のストリームに書き込むことができます。 非同期 IO を効果的に使用するには多大な労力が必要です。正しく行われなければ、パフォーマンスが 低下 する可能性があります。 利点は、正しく適用すると、非同期 IO によってパフォーマンスが最大 10 倍に向上する可能性がある点です。

非同期 IO を使用するプログラムの優れた例については、MSDN ライブラリを参照してください。

  • 注意すべき点の 1 つは、非同期呼び出しのセキュリティ オーバーヘッドが小さいことです。非同期呼び出しを呼び出すと、呼び出し元のスタックのセキュリティ状態がキャプチャされ、実際に要求を実行するスレッドに転送されます。 これは、コールバックが大量のコードを実行する場合、または非同期呼び出しが過剰に使用されない場合は、問題にならない可能性があります

データベース アクセスに関するヒント

データベース アクセスのチューニングの理念は、必要な機能のみを使用し、"切断された" アプローチを中心に設計することです。1 つの接続を長時間開いたままにするのではなく、複数の接続を順番に作成します。 この変更を考慮し、その周りを設計する必要があります。

Microsoft では、クライアントからデータベースへの直接接続ではなく、パフォーマンスを最大限に高めるために N 層戦略を推奨しています。 多くのテクノロジが多く疲れるシナリオを利用するように最適化されているので、これを設計哲学の一部と考えてください。

最適なマネージド プロバイダーを使用する

汎用アクセサーに依存するのではなく、マネージド プロバイダーを正しく選択してください。 SQL (System.Data.SqlClient) など、さまざまなデータベース用に特別に記述されたマネージド プロバイダーがあります。 特殊なコンポーネントを使用できる場合に System.Data.Odbc などのより汎用的なインターフェイスを使用すると、追加された間接参照レベルを処理するパフォーマンスが失われます。 最適なプロバイダーを使用すると、別の言語を話すこともできます。マネージド SQL クライアントは TDS を SQL データベースに読み上げ、汎用 OleDbprotocol よりも大幅に向上します。

可能な場合は、データ セット上のデータ リーダーを選択します

データを保持する必要がない場合は常にデータ リーダーを使用します。 これにより、データの高速読み取りが可能になり、ユーザーが必要に応じてキャッシュできます。 リーダーは単にステートレス ストリームであり、データの到着時にデータを読み取り、より多くのナビゲーションのためにデータセットに保存せずに削除できます。 ストリームアプローチは、データの使用をすぐに開始できるため、高速でオーバーヘッドが少なくなります。 ナビゲーションのキャッシュが適しているかどうかを判断するために、同じデータが必要な頻度を評価する必要があります。 サーバーからデータをプルする場合の ODBC プロバイダーと SQL プロバイダーの両方の DataReader と DataSet の違いを示す小さな表を次に示します (数値が大きいほど良くなります)。

  ADO (ADO) SQL
DataSet 801 2507
DataReader 1083 4585

ご覧のように、最適なマネージド プロバイダーとデータ リーダーを使用すると、最高のパフォーマンスが実現されます。 データをキャッシュする必要がない場合は、データ リーダーを使用すると、パフォーマンスを大幅に向上させることができます。

MP マシンにMscorsvr.dllを使用する

スタンドアロンの中間層アプリケーションとサーバー アプリケーションの場合は、マルチプロセッサ マシンで が使用されていることを確認 mscorsvr してください。 Mscorwks はスケーリングまたはスループットに最適化されていませんが、サーバー バージョンには、複数のプロセッサが使用可能な場合に適切にスケーリングできるいくつかの最適化があります。

可能な限りストアド プロシージャを使用する

ストアド プロシージャは高度に最適化されたツールであり、効果的に使用すると優れたパフォーマンスが得られます。 データ アダプターを使用して挿入、更新、および削除を処理するストアド プロシージャを設定します。 ストアド プロシージャは、クライアントから解釈、コンパイル、または送信する必要はありません。また、ネットワーク トラフィックとサーバーのオーバーヘッドの両方を削減できます。 CommandType.Text の代わりに CommandType.StoredProcedure を使用してください

動的接続文字列には注意してください

接続プールは、各要求の接続を開いたり閉じたりするオーバーヘッドを支払うのではなく、複数の要求の接続を再利用するのに便利な方法です。 暗黙的に行われますが、 一意の接続文字列ごとに 1 つのプールが取得されます。 接続文字列を動的に生成する場合は、プールが発生するたびに文字列が同じであることを確認します。 また、委任が行われている場合は、ユーザーごとに 1 つのプールが取得されます。 接続プールに対して設定できるオプションは多数あり、Perfmon を使用して応答時間、トランザクション/秒などを追跡することで、プールのパフォーマンスを追跡できます。

使用しない機能をオフにする

必要がない場合は、自動トランザクション参加リストをオフにします。 SQL マネージド プロバイダーの場合は、接続文字列を使用して行われます。

SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");

データセットにデータ アダプターを入力するときに、必要がない場合は主キー情報を取得しないでください (たとえば、MissingSchemaAction.Add をキーで設定しないでください)。

public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

自動生成されたコマンドを回避する

データ アダプターを使用する場合は、自動生成されたコマンドを使用しないでください。 これには、メタデータを取得し、より低いレベルの対話制御を提供するために、サーバーへの追加のトリップが必要です。 自動生成されたコマンドを使用すると便利ですが、パフォーマンスが重要なアプリケーションで自分で実行する価値があります。

ADO レガシ デザインに注意する

アダプターでコマンドまたは呼び出しフィルを実行すると、クエリで指定されたすべてのレコードが返されます。

サーバー カーソルが絶対に必要な場合は、t-sql のストアド プロシージャを使用して実装できます。 サーバーカーソルベースの実装はあまりうまくスケーリングされないため、可能な限り避けてください。

必要に応じて、ステートレスおよびコネクションレスの方法でページングを実装します。 次の方法で、データセットにレコードを追加できます。

  • PK 情報が存在することを確認する
  • 必要に応じてデータ アダプターの select コマンドを変更し、
  • Fill の呼び出し

データセットのリーンを維持する

必要なレコードのみをデータセットに配置します。 データセットは、すべてのデータをメモリに格納し、要求するデータが多いほど、ネットワーク経由での送信にかかる時間が長くなることに注意してください。

可能な限り頻繁にシーケンシャル アクセスを使用する

データ リーダーでは、CommandBehavior.SequentialAccess を使用します。 これは、データを小さなチャンクでワイヤから読み取ることができるため、BLOB データ型を処理するために不可欠です。 一度に操作できるデータは 1 つだけですが、大きなデータ型を読み込むための待機時間は消えます。 オブジェクト全体を一度に処理する必要がない場合は、シーケンシャル アクセスを使用すると、パフォーマンスが大幅に向上します。

ASP.NET アプリケーションのパフォーマンスに関するヒント

積極的にキャッシュする

ASP.NET を使用してアプリを設計する場合は、キャッシュを念頭に置いて設計してください。 OS のサーバー バージョンでは、サーバー側とクライアント側でのキャッシュの使用を調整するための多くのオプションがあります。 ASP には、パフォーマンスを得るために使用できるいくつかの機能とツールがあります。

出力キャッシュ — ASP 要求の静的な結果を格納します。 ディレクティブを使用して指定します <@% OutputCache %>

  • 期間 - キャッシュに時間項目が存在する
  • VaryByParam - Get/Post パラメーターによってキャッシュ エントリを変更します
  • VaryByHeader - Http ヘッダーによってキャッシュ エントリを変更します
  • VaryByCustom — ブラウザーごとにキャッシュ エントリを変更します
  • をオーバーライドして、必要な内容によって異なります。
    • フラグメント キャッシュ - ページ全体 (プライバシー、パーソナル化、動的コンテンツ) を格納できない場合は、フラグメント キャッシュを使用して一部を格納し、後ですばやく取得できます。

      a) VaryByControl - コントロールの値によってキャッシュされた項目を変更します

    • キャッシュ API — キャッシュされたオブジェクトのハッシュテーブルをメモリに保持することで、キャッシュに非常に細かい粒度を提供します (System.web.UI.caching)。 また、次のことも行います。

      a) 依存関係 (キー、ファイル、時刻) が含まれます

      b) 未使用のアイテムが自動的に期限切れになる

      c) コールバックをサポート

インテリジェントにキャッシュすると優れたパフォーマンスが得られます。また、必要なキャッシュの種類について考える必要があります。 ログイン用の静的ページが複数ある複雑な e コマース サイトと、画像とテキストを含む動的に生成されたページのスルーを想像してみてください。 これらのログイン ページには出力キャッシュを使用し、動的ページにはフラグメント キャッシュを使用できます。 たとえば、ツール バーはフラグメントとしてキャッシュできます。 パフォーマンスをさらに向上させるには、Cache API を使用して、サイトに頻繁に表示される一般的に使用されるイメージと定型テキストをキャッシュできます。 キャッシュの詳細 (サンプル コードを使用) については、ASP. NET Web サイトをチェック。

必要な場合にのみセッション状態を使用する

ASP.NET の非常に強力な機能の 1 つは、eコマース サイトのショッピング カートやブラウザー履歴など、ユーザーのセッション状態を格納する機能です。 これは既定でオンになっているため、使用しない場合でもメモリ内のコストを支払います。 セッション状態を使用していない場合は、@% EnabledSessionState = false %> を asp に追加<することで、セッション状態をオフにし、オーバーヘッドを節約します。 これには、 ASP. NET Web サイトで説明されている他のいくつかのオプションが含まれます。

セッション状態のみを読み取るページの場合は、[ EnabledSessionState=readonly] を選択できます。 これは、完全な読み取り/書き込みセッション状態よりもオーバーヘッドが少なく、機能の一部のみが必要で、書き込み機能の料金を支払いたくない場合に便利です。

ビュー ステートは、次の場合にのみ使用します。

ビューステートの例としては、ユーザーが入力する必要がある長いフォームがあります。ブラウザーで [ 戻る ] をクリックして戻った場合、フォームは入力されたままになります。 この機能を使用しない場合、この状態ではメモリとパフォーマンスが低下します。 おそらく、最大のパフォーマンスドレインは、ページが読み込まれるたびにネットワーク経由でラウンドトリップ信号を送信してキャッシュを更新して検証する必要があるということです。 既定ではオンになっているため、@% EnabledViewState = false %>でビューステート<を使用しないように指定する必要があります。 ASP. NET Web サイトのビューステートの詳細については、アクセスできるその他のオプションと設定の一部について学習する必要があります。

STA COM を回避する

Apartment COM は、アンマネージド環境でのスレッド処理を処理するように設計されています。 アパートメント COM には、シングル スレッドとマルチスレッドの 2 種類があります。 MTA COM はマルチスレッドを処理するように設計されています。一方、STA COM はメッセージング システムに依存してスレッド要求をシリアル化します。 マネージド ワールドはフリー スレッドであり、シングル スレッド アパートメント COM を使用するには、すべてのアンマネージド スレッドが基本的に相互運用のために 1 つのスレッドを共有する必要があります。 これにより、パフォーマンスに 大きな ヒットが生じ、可能な限り回避する必要があります。 Apartment COM オブジェクトをマネージド ワールドに移植できない場合は、それらを使用するページに @%AspCompat = "true" % を>使用<します。 STA COM の詳細については、MSDN ライブラリを参照してください。

バッチ コンパイル

大きなページを Web にデプロイする前に、常にバッチ コンパイルします。 これは、ディレクトリごとに 1 つのページに対して 1 つの要求を実行し、CPU が再びアイドル状態になるまで待機することによって開始できます。 これにより、ページを提供しようとしているときに、Web サーバーがコンパイルで行き詰まるのを防ぐことができます。

不要な Http モジュールを削除する

使用されている機能に応じて、パイプラインから未使用または不要な http モジュールを削除します。 追加されたメモリと無駄なサイクルを再利用することで、小さな速度ブーストを実現できます。

Autoeventwireup 機能を回避する

autoeventwireup に依存する代わりに、Page のイベントをオーバーライドします。 たとえば、 Page_Load() メソッドを記述する代わりに、public void OnLoad() メソッドをオーバーロードしてみてください。 これにより、実行時にすべてのページに 対して CreateDelegate() を実行する必要があります。

UTF が不要な場合に ASCII を使用してエンコードする

既定では、ASP.NET は、要求と応答を UTF-8 としてエンコードするように構成されます。 ASCII がアプリケーションのニーズをすべて満たしていれば、UTF オーバーヘッドを排除すると、いくつかのサイクルが返される可能性があります。 これは、アプリケーションごとにのみ実行できることに注意してください。

最適な認証手順を使用する

ユーザーを認証する方法はいくつかありますが、他の方法よりもコストが高いものがあります (コストを増やすには、None、Windows、Forms、Passport)。 ニーズに最も適した最も安いものを使用してください。

Visual Basic での移植と開発に関するヒント

内部で Microsoft Visual Basic 6 から Microsoft® Visual Basic®® 7 に®多くの変更が行われ、それに伴ってパフォーマンス マップが変更されました。 CLR の機能とセキュリティの制限が追加されているため、一部の関数は Visual Basic 6 と同じように迅速に実行できません。 実際には、Visual Basic 7 が前身によって引き継がれる領域がいくつかあります。 幸いなことに、次の 2 つの良いニュースがあります。

  • 最悪の速度低下のほとんどは、初めてコントロールを読み込むなど、1 回限りの関数で発生します。 コストはそこにありますが、1 回だけ支払います。
  • Visual Basic 7 の方が高速な領域が多数あり、これらの領域は実行時に繰り返される関数に存在する傾向があります。 つまり、特典は時間の経過と同時に増加し、場合によっては 1 回限りのコストを上回ります。

パフォーマンスの問題の大部分は、実行時に Visual Basic 6 の機能がサポートされていない領域から発生し、Visual Basic 7 で機能を保持するために追加する必要があります。 実行時以外の作業は遅くなり、一部の機能の使用コストがはるかに高くなります。 明るい面は、少しの労力でこれらの問題を回避できることです。 パフォーマンスを最適化するために作業を必要とする 2 つのメイン領域と、ここで行うことができる簡単な調整がいくつかあります。 これらをまとめると、パフォーマンスの低下を回避し、Visual Basic 7 ではるかに高速な関数を利用するのに役立ちます。

エラー処理

最初の懸念事項は、エラー処理です。 これは Visual Basic 7 で大幅に変更され、変更に関連するパフォーマンスの問題があります。 基本的に、 OnErrorGotoResume を実装するために必要なロジックは非常に高価です。 コードを簡単に見て、 Err オブジェクトまたはエラー処理メカニズムを使用するすべての領域を強調表示することをお勧めします。 次に、これらの各インスタンスを確認し、 try/catch を使用するように書き換えることができるかどうかを確認します。 多くの開発者は、ほとんどの場合、 簡単に try/catch に変換できることに気が付き、プログラムのパフォーマンスが向上しているはずです。 経験則は、「翻訳を簡単に見ることができる場合は、それを行う」です。

Try/catch バージョンと比較して On Error Goto を使用する単純な Visual Basic プログラムの例を次に示します。

Sub SubWithError()
On Error Goto SWETrap
  Dim x As Integer
  Dim y As Integer
  x = x / y
SWETrap:  Exit Sub
  End Sub
 
Sub SubWithErrorResumeLabel()
  On Error Goto SWERLTrap
  Dim x As Integer
  Dim y As Integer
  x = x / y 
SWERLTrap:
  Resume SWERLExit
  End Sub
SWERLExit:
  Exit Sub
Sub SubWithError()
  Dim x As Integer
  Dim y As Integer
  Try    x = x / y  Catch    Return  End Try
  End Sub
 
Sub SubWithErrorResumeLabel()
  Dim x As Integer
  Dim y As Integer
  Try
    x = x / y
  Catch
  Goto SWERLExit
  End Try
 
SWERLExit:
  Return
  End Sub

速度の増加は顕著です。 SubWithError()OnErrorGoto を使用して 244 ミリ秒、 try/catch を使用する場合は 169 ミリ秒しかかかりません。 2 番目の関数は、最適化されたバージョンでは 164 ミリ秒と比較して 179 ミリ秒かかります。

事前バインディングを使用する

2 つ目の懸念事項は、オブジェクトと型キャストを扱います。 Visual Basic 6 では、オブジェクトのキャストをサポートするために内部で多くの作業が行われ、多くのプログラマはそれを認識していません。 Visual Basic 7 では、これは、その中から多くのパフォーマンスを絞り込むことができる領域です。 コンパイルするときは、 事前バインディングを使用します。 これにより、 型強制型 変換を挿入するようにコンパイラに指示されるのは、明示的に言及されている場合のみです。 これには、次の 2 つの主な効果があります。

  • 奇妙なエラーを追跡しやすくなります。
  • 不要な強制が排除され、パフォーマンスが大幅に向上します。

オブジェクトを別の型であるかのように使用する場合、指定しない場合は、Visual Basic によってオブジェクトが強制されます。 プログラマは少ないコードについて心配する必要があるため、これは便利です。 欠点は、これらの強制型は予期しないことを行う可能性があり、プログラマはそれらを制御できないということです。

遅延バインディングを使用する必要がある場合はインスタンスがありますが、わからない場合はほとんどの場合、早期バインディングを使い切ることができます。 Visual Basic 6 プログラマの場合、以前よりも型について心配する必要があるため、最初は少し厄介になる可能性があります。 これは新しいプログラマにとって簡単なはずです。Visual Basic 6 に精通しているユーザーは、それを短時間で取得できます。

オプション Strict と Explicit をオンにする

Option Strict をオンにすると、不注意による遅延バインディングから身を守り、より高いレベルのコーディング規範を適用できます。 Option Strict に関する制限事項の一覧については、MSDN ライブラリを参照してください。 これに関する注意事項は、すべての縮小型強制型を明示的に指定する必要があるということです。 ただし、これはそれ自体が、以前に考えていたよりも多くの作業を行っているコードの他のセクションを明らかにし、プロセス内のいくつかのバグを踏み込むのに役立つ場合があります。

Option Explicit は Option Strict よりも制限が厳しくありませんが、プログラマはコードでより多くの情報を提供するように強制されます。 具体的には、変数を使用する前に宣言する必要があります。 これにより、型推論が実行時からコンパイル時に移動されます。 これにより、チェックがなくなり、パフォーマンスが向上します。

Option Explicit から始めて、Option Strict をオンにすることをお勧めします。 これにより、コンパイラ エラーの多くから保護され、より厳密な環境で徐々に作業を開始できます。 これらの両方のオプションを使用する場合は、アプリケーションのパフォーマンスを最大限に高めます。

テキストにバイナリ比較を使用する

テキストを比較する場合は、テキスト比較の代わりにバイナリ比較を使用します。 実行時には、バイナリのオーバーヘッドははるかに軽くなります。

Format() の使用を最小限に抑える

可能な場合は、format()の代わりに toString() を使用します。 ほとんどの場合、必要な機能が提供され、オーバーヘッドははるかに少なくなります。

Charw を使用する

char の代わりに charw を使用します。 CLR は Unicode を内部的に使用し、char を使用する場合は実行時に変換する必要があります。 これにより、パフォーマンスが大幅に低下する可能性があり、文字が完全な単語長であることを指定します (charw) を使用すると、この変換は不要になります。

割り当ての最適化

exp = exp + val の代わりに exp += val を使用します。 exp は任意に複雑になる可能性があるため、多くの不要な作業が発生する可能性があります。 これにより、JIT は exp の両方のコピーを強制的に評価します。これは何度も必要ありません。 最初のステートメントは 2 番目の ステートメントよりもはるかに適切に最適化できます。JIT では exp の評価が 2 回回避される可能性があるためです。

不要な間接参照を回避する

byRef を使用する場合は、実際のオブジェクトではなくポインターを渡します。 多くの場合、これは理にかなっていますが (副作用のある関数など)、必ずしも必要とは限りません。 ポインターを渡すと間接参照が多くなり、スタック上の値にアクセスするよりも遅くなります。 ヒープを通過する必要がない場合は、それを避けるのが最善です。

連結を 1 つの式に配置する

複数の行に複数の連結がある場合は、それらをすべて 1 つの式に貼り付けてみてください。 コンパイラは、適切な文字列を変更し、速度とメモリブーストを提供することで最適化できます。 ステートメントが複数行に分割されている場合、Visual Basic コンパイラは、インプレース連結を可能にする Microsoft Intermediate Language (MSIL) を生成しません。 前に説明した StringBuilder の例を参照してください。

Return ステートメントを含める

Visual Basic を使用すると、 関数は return ステートメントを使用せずに値を返すことができます。 Visual Basic 7 ではこれをサポートしていますが、 明示的に return を使用すると、JIT でもう少し最適化を実行できます。 return ステートメントがない場合、各関数には、キーワード (keyword)なしで透過的に戻り値をサポートするために、スタック上のいくつかのローカル変数が与えられます。 これらを維持すると、JIT の最適化が困難になり、コードのパフォーマンスに影響を与える可能性があります。 関数を調び、必要に応じて 戻り値 を挿入します。 これはコードのセマンティクスをまったく変更せず、アプリケーションからより多くの速度を得るのに役立ちます。

マネージド C++ での移植と開発に関するヒント

Microsoft は、特定の開発者セットで Managed C++ (MC++) を対象としています。 MC++ はすべての ジョブに最適なツールではありません。 このドキュメントを読んだ後、C++ が最適なツールではなく、トレードオフのコストがメリットを得る価値はないことを判断できます。 MC++ について不明な点がある場合は、決定に役立つ多くの優れた リソース があります。このセクションは、MC++ を何らかの方法で使用することを既に決定し、そのパフォーマンスの側面について知りたいと考えている開発者を対象としています。

C++ 開発者の場合、マネージ C++ を動作させるには、いくつかの決定を行う必要があります。 古いコードを移植していますか? その場合、全体を管理領域に移動しますか、それともラッパーの実装を計画していますか? 私は「port-everything」オプションに焦点を当てるか、この議論の目的のためにMC++をゼロから書くことを扱うつもりです。これらはプログラマがパフォーマンスの違いに気付くシナリオであるためです。

マネージド ワールドの利点

マネージド C++ の最も強力な機能は、マネージド コードとアンマネージド コードを 式レベルで混在させ、一致させる機能です。 他の言語ではこれを実行できません。適切に使用すれば、そこから得られる強力な利点がいくつかあります。 これについては、後でいくつか例を見ていきます。

マネージド ワールドでは、多くの一般的な問題が処理されるという点で、設計上の大きな勝利も得られます。 メモリ管理、スレッド スケジューリング、型強制は、必要に応じて実行時間に任せるので、必要なプログラムの部分にエネルギーを集中させることができます。 MC++ を使用すると、保持するコントロールの量を正確に選択できます。

MC++ プログラマは、IL にコンパイルするときに Microsoft Visual C® 7 (VC7) バックエンドを使用し、その上に JIT を使用できるという贅沢な機能を備えています。 Microsoft C++ コンパイラの操作に慣れているプログラマは、高速な処理に慣れます。 JIT はさまざまな目標を持って設計されており、長所と短所のセットが異なります。 JIT の時間制限に拘束されない VC7 コンパイラは、プログラム全体の分析、より積極的なインライン化、登録など、JIT で実行できない特定の最適化を実行できます。 また、型セーフ環境でのみ実行できる最適化もあり、C++ で許可されているよりも速度の余地が大きくなっています。

JIT の優先順位が異なるため、一部の操作は以前よりも高速ですが、他の操作は遅くなります。 安全性と言語の柔軟性のためにトレードオフがあり、その一部は安くありません。 幸いなことに、プログラマがコストを最小限に抑えるためにできることがあります。

移植: すべての C++ コードを MSIL にコンパイルできます

先に進む前に、 任意 の C++ コードを MSIL にコンパイルできることに注意することが重要です。 すべてが機能しますが、タイプセーフの保証はなく、多くの相互運用を行う場合はマーシャリングペナルティを支払います。 利点がない場合、MSIL にコンパイルすると便利なのはなぜですか? 大規模なコード ベースを移植する状況では、コードを段階的に個別に移植できます。 MC++ を使用する場合は、移植されたコードとまだ移植されていないコードを接着する特別なラッパーを記述するのではなく、より多くのコードの移植に時間を費やすことができます。その結果、大きな勝利が生じる可能性があります。 これにより、アプリケーションの移植は非常にクリーンプロセスになります。 C++ を MSIL にコンパイルする方法の詳細については、 /clr コンパイラ オプションを参照してください。

ただし、単に C++ コードを MSIL にコンパイルするだけでは、マネージド ワールドのセキュリティや柔軟性は得られません。 MC++ で を記述する必要があります。v1 では、いくつかの機能をあきらめる必要があります。 次の一覧は、CLR の現在のバージョンではサポートされていませんが、今後サポートされる可能性があります。 Microsoft は、最も一般的な機能を最初にサポートすることを選択し、出荷するために他の機能を削減する必要がありました。 後で追加することを妨げるものは何もありませんが、それまでの間は、それらを使用せずに行う必要があります。

  • 多重継承
  • テンプレート
  • 確定的なファイナライズ

これらの機能が必要な場合は、常に安全でないコードと相互運用できますが、データのマーシャリングのパフォーマンスが低下します。 また、これらの機能はアンマネージド コード内でのみ使用できることに注意してください。 マネージド スペースには、その存在に関する知識がありません。 コードを移植することを決定する場合は、設計でこれらの機能にどの程度依存しているかを考えてください。 場合によっては、再設計が高すぎるため、アンマネージド コードを使用する必要があります。 これは、ハッキングを開始する前に行う必要がある最初の決定です。

C# または Visual Basic よりも MC++ の利点

アンマネージド バックグラウンドから取得した MC++ では、安全でないコードを処理する多くの機能が保持されます。 マネージ コードとアンマネージド コードを混在させる MC++の機能は、開発者に多くの力を提供し、コードを記述するときにグラデーション上のどこに座りたいかを選択できます。 極端な場合は、すべてをストレートで非adulterated C++ で記述し、 単に /clr を使用してコンパイルできます。 一方、すべてをマネージド オブジェクトとして記述し、上記の言語制限とパフォーマンスの問題に対処できます。

ただし、MC++ の実際の機能は、その中間のどこかを選択するときに発生します。 MC++ を使用すると、安全でない機能を使用するタイミングを正確に制御できるため、マネージド コードに固有のパフォーマンス ヒットの一部を微調整できます。 C# には安全でないキーワード (keyword)にこの機能がいくつかありますが、言語の不可欠な部分ではなく、MC++ よりもはるかに役に立ちません。 MC++ で使用できる細かい粒度を示す例をいくつか見てみましょう。ここでは、それが役に立つ状況について説明します。

一般化された "byref" ポインター

C# では、 ref パラメーターに渡すことによってのみ、クラスのメンバーのアドレスを取得できます。 MC++ では、byref ポインターはファースト クラスのコンストラクトです。 配列の途中にある項目のアドレスを取得し、そのアドレスを関数から返すことができます。

Byte* AddrInArray( Byte b[] ) {
   return &b[5];
}

この機能を利用して、ヘルパー ルーチンを介して System.String 内の "文字" へのポインターを返します。また、これらのポインターを使用して配列をループすることもできます。

System::Char* PtrToStringChars(System::String*);   
for( Char*pC = PtrToStringChars(S"boo");
  pC != NULL;
  pC++ )
{
      ... *pC ...
}

MC++ で挿入を使用してリンクリストトラバーサルを実行することもできます。"next" フィールドのアドレスを取得します (C#では実行できません)。

Node **w = &Head;
while(true) {
  if( *w == 0 || val < (*w)->val ) {
    Node *t = new Node(val,*w);
    *w = t;
    break;
  }
  w = &(*w)->next;
}

C# では、"Head" をポイントしたり、"next" フィールドのアドレスを取得したりすることはできません。そのため、最初の場所に挿入する特殊なケースを作成したり、"Head" が null の場合を指定したりします。 さらに、コード内で常に 1 つのノードを先に見る必要があります。 これを、適切な C# で生成される内容と比較します。

if( Head==null || val < Head.val ) {
  Node t = new Node(val,Head);
  Head = t;
}else{
  // we know at least one node exists,
  // so we can look 1 node ahead
  Node w=Head;
while(true) {
  if( w.next == null || val < w.next.val ){
    Node t = new Node(val,w.next.next);
    w.next = t;
    break;
  }
  w = w.next;
  }
}         

ボックス化された型へのユーザー アクセス

OO 言語で一般的なパフォーマンスの問題は、値のボックス化とボックス化解除に費やされた時間です。 MC++ を使用すると、この動作をより細かく制御できるため、値にアクセスするために動的に (または静的に) ボックスを外す必要はありません。 これは、もう 1 つのパフォーマンス向上です。 ボックス化されたフォーム__box表す型の前にキーワード (keyword)を配置するだけです。

__value struct V {
  int i;
};
int main() {
  V v = {10};
  __box V *pbV = __box(v);
  pbV->i += 10;           // update without casting
}

C# では、"v" にボックス化を解除し、値を更新して Object に再ボックス化する必要があります。

struct B { public int i; }
static void Main() {
  B b = new B();
  b.i = 5;
  object o = b;         // implicit box
  B b2 = (B)o;            // explicit unbox
  b2.i++;               // update
  o = b2;               // implicit re-box
}

STL コレクションとマネージド コレクション —v1

悪いニュース: C++ では、STL コレクションの使用は、多くの場合、その機能を手動で記述するのと同じくらい高速でした。 CLR フレームワークは非常に高速ですが、ボックス化とボックス化解除の問題に悩まされます。すべてがオブジェクトであり、テンプレートまたは汎用サポートがない場合は、実行時にすべてのアクションをチェックする必要があります。

良いニュース: 長期的には、ジェネリックが実行時に追加されると、この問題は解決する可能性があります。 現在デプロイするコードでは、変更なしで速度が向上します。 短期的には、静的キャストを使用してチェックを防ぐことができますが、これは安全ではなくなります。 パフォーマンスが絶対に重要であり、2 つまたは 3 つのホット スポットを特定したタイトなコードで、このメソッドを使用することをお勧めします。

スタック マネージド オブジェクトを使用する

C++ では、オブジェクトをスタックまたはヒープで管理することを指定します。 MC++ では引き続きこれを行うことができますが、注意する必要がある制限があります。 CLR では、すべてのスタックマネージド オブジェクトに対して ValueTypes が使用されます。また、ValueTypes で実行できる操作には制限があります (継承は行われません)。 詳細については 、MSDN ライブラリを参照してください。

コーナー ケース: マネージド コード内の間接呼び出しに注意する —v1

v1 ランタイムでは、すべての間接関数呼び出しがネイティブに行われるため、アンマネージド空間への移行が必要です。 間接関数呼び出しはネイティブ モードからのみ行うことができます。つまり、マネージド コードからのすべての間接呼び出しには、マネージドからアンマネージドへの移行が必要です。 これは、テーブルがマネージド関数を返すときに重大な問題です。これは、関数を実行するために 2 つ目 の遷移を行う必要があるためです。 1 つの 呼び出し 命令を実行するコストと比較すると、コストは C++ よりも 50 倍から 100 倍遅くなります。

幸いなことに、ガベージ コレクション クラス内に存在するメソッドを呼び出すと、最適化によってこの処理が削除されます。 ただし、 /clr を使用してコンパイルされた通常の C++ ファイルの特定のケースでは、メソッドの戻り値はマネージドと見なされます。 これは最適化では削除できないため、完全な二重移行コストが発生します。 このようなケースの例を次に示します。

//////////////////////// a.h:    //////////////////////////
class X {
public:
   void mf1();
   void mf2();
};

typedef void (X::*pMFunc_t)();


////////////// a.cpp: compiled with /clr  /////////////////
#include "a.h"

int main(){
   pMFunc_t pmf1 = &X::mf1;
   pMFunc_t pmf2 = &X::mf2;

   X *pX = new X();
   (pX->*pmf1)();
   (pX->*pmf2)();

   return 0;
}


////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"

void X::mf1(){}


////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}

これを回避するには、いくつかの方法があります。

  • クラスをマネージド クラスにする ("__gc")
  • 可能な場合は間接呼び出しを削除します
  • クラスをアンマネージド コードとしてコンパイルしたままにしておきます (例: /clr を使用しない)

パフォーマンス ヒットの最小化 - バージョン 1

バージョン 1 の JIT では、MC++ ではコストが高くなる操作や機能がいくつかあります。 私はそれらを一覧表示し、いくつかの説明を与え、我々はあなたがそれらについて何ができるかについて話します。

  • 抽象化 - これは、緩やかに遅い C++ バックエンド コンパイラが JIT よりも大きく優先される領域です。 抽象化のために int をクラス内でラップし、厳密に int としてアクセスする場合、C++ コンパイラはラッパーのオーバーヘッドを実質的に何も削減できません。 コストを増やすことなく、ラッパーに多くのレベルの抽象化を追加できます。 JIT では、このコストをなくすために必要な時間を取ることができず、MC++ では深い抽象化のコストが高くなります。
  • 浮動小数点 - v1 JIT は現在、VC++ バックエンドが実行するすべての FP 固有の最適化を実行するわけではないため、現時点では浮動小数点操作のコストが高くなります。
  • 多次元配列 - JIT は多次元配列よりもジャグ配列を処理する方が優れているため、代わりにジャグ配列を使用します。
  • 64 ビット算術 - 今後のバージョンでは、64 ビットの最適化が JIT に追加されます。

できること

開発の各フェーズで、いくつかの操作を実行できます。 MC++ では、最終的に実行する作業量と、その結果として得られるパフォーマンスが決まるため、設計フェーズはおそらく最も重要な領域です。 アプリケーションを書き込むか移植する場合は、次の点を考慮する必要があります。

  • 複数の継承、テンプレート、または確定的な最終処理を使用する領域を特定します。 これらを取り除くか、コードのその部分をアンマネージド領域に残す必要があります。 再設計のコストを考え、移植できる領域を特定します。
  • マネージド 空間全体のディープ 抽象化や仮想関数呼び出しなど、パフォーマンス のホット スポットを見つけます。 また、設計上の決定も必要です。
  • スタック管理として指定されているオブジェクトを探します。 ValueTypes に変換できることを確認します。 他のユーザーにヒープマネージド オブジェクトへの変換をマークします。

コーディング段階では、よりコストの高い操作と、それらを処理する際のオプションに注意する必要があります。 MC++ の最も良い点の 1 つは、コーディングを開始する前に、パフォーマンスに関するすべての問題を事前に把握しておく必要があるということです。これは、後で作業を縮小する際に役立ちます。 ただし、コードとデバッグ中に実行できる微調整はまだあります。

浮動小数点演算、多次元配列、またはライブラリ関数を大量に使用する領域を決定します。 これらの領域のうち、パフォーマンスが重要な領域はどれですか? プロファイラーを使用して、オーバーヘッドのコストが最も高いフラグメントを選択し、最適と思われるオプションを選択します。

  • フラグメント全体をアンマネージド空間に保持します。
  • ライブラリ アクセスで静的キャストを使用します。
  • ボックス化/ボックス化解除の動作を調整してみてください (後で説明します)。
  • 独自の構造をコーディングします。

最後に、行う切り替えの数を最小限に抑えます。 何らかのアンマネージド コードまたは相互運用呼び出しがループ内にある場合は、ループ全体をアンマネージドにします。 この方法では、ループの反復ごとにではなく、移行コストを 2 回だけ支払います。

その他のリソース

.NET Frameworkのパフォーマンスに関する関連トピックは次のとおりです。

設計、アーキテクチャ、コーディングの哲学の概要、マネージド世界でのパフォーマンス分析ツールのチュートリアル、現在利用可能な他のエンタープライズ アプリケーションとの .NET のパフォーマンス比較など、現在開発中の今後の記事をご覧ください。

付録: 仮想呼び出しと割り当てのコスト

通話の種類 # Calls/sec
ValueType 非仮想呼び出し 809971805.600
クラス非仮想呼び出し 268478412.546
クラス仮想呼び出し 109117738.369
ValueType Virtual (Obj メソッド) 呼び出し 3004286.205
ValueType Virtual (オーバーライドされた Obj メソッド) 呼び出し 2917140.844
新しい読み込みによる型の読み込み (非静的) 1434.720
Newing による読み込み型 (仮想メソッド) 1369.863

メモ テスト マシンは PIII 733Mhz で、Windows 2000 Professional With Service Pack 2 が実行されています。

このグラフでは、さまざまな種類のメソッド呼び出しに関連するコストと、仮想メソッドを含む型をインスタンス化するコストを比較します。 数値が大きいほど、1 秒あたりの呼び出し/インスタンス化数が多くなります。 これらの数値はマシンや構成によって異なりますが、ある呼び出しを別の呼び出しに対して実行する相対的なコストは依然として重要です。

  • ValueType 非仮想呼び出し: このテストでは、ValueType 内に含まれる空の非仮想メソッドを呼び出します。
  • クラス非仮想呼び出し: このテストでは、クラス内に含まれる空の非仮想メソッドを呼び出します。
  • クラス仮想呼び出し: このテストでは、クラス内に含まれる空の仮想メソッドを呼び出します。
  • ValueType Virtual (Obj メソッド) 呼び出し: このテストでは、ValueType に対して ToString() () (仮想メソッド) を呼び出します。このメソッドは、既定のオブジェクト メソッドに依存します。
  • ValueType Virtual (Overridden Obj メソッド) 呼び出し: このテストでは、既定値をオーバーライドした ValueType で ToString() (仮想メソッド) を呼び出します。
  • Load Type by Newing (Static): このテストでは、静的メソッドのみを使用してクラスに領域を割り当てます。
  • Load Type by Newing (Virtual Methods): このテストでは、仮想メソッドを使用してクラスの領域を割り当てます。

1 つの結論として、 Virtual Function 呼び出しは、クラス内のメソッドを呼び出すときの通常の呼び出しの約 2 倍のコストがかかるということです。 呼び出しは最初は安いので、すべての仮想呼び出しを削除するわけではありません。 これを行うのが理にかなっている場合は、常に仮想メソッドを使用する必要があります。

  • JIT では仮想メソッドをインライン化できないため、仮想以外のメソッドを取り除くと、潜在的な最適化が失われます。
  • 仮想メソッドを持つオブジェクトの領域の割り当ては、仮想テーブルの領域を見つけるために余分な作業を行う必要があるため、オブジェクトの割り当てよりも少し遅くなります。

ValueType 内で非仮想メソッドを呼び出すと、クラスの 3 倍以上の速度が得られますが、クラス として 扱うと、大きな損失が発生します。 これは ValueTypes の特性です。構造体のように扱うと、高速に点灯します。 クラスのように扱うと、痛みを伴うほど遅くなります。 ToString() は仮想メソッドであるため、呼び出す前に、構造体をヒープ上のオブジェクトに変換する必要があります。 2 倍遅くなる代わりに、ValueType で仮想メソッドを呼び出すのが 18 倍遅くなりました。 物語のモラル? ValueTypes はクラスとして扱いません。

この記事に関する質問やコメントがある場合は、プログラム マネージャーの Claudio Caldato に.NET Frameworkパフォーマンスの問題についてお問い合わせください。