並列コンピューティング

データ処理: 並列処理とパフォーマンス

Johnson M. Hart

コード サンプルのダウンロード

データのコレクションを処理することは、コンピューター処理の基本タスクの 1 つです。このタスクの実質的問題の多くを本質的に並列処理にすることで、マルチコア システムでのパフォーマンスとスループットが向上する可能性があります。今回は、データを高度に並列処理することで問題を解決する Windows ベースの手法をいくつか比較します。

この比較に使用するベンチマークは、Troy Magennis 氏の著書『LINQ to Objects Using C# 4.0』(Addison-Wesley、2010 年) の第 9 章の検索の問題 (「Geonames」) からの引用です。以下に比較するソリューションを示します。

  • PLINQ (Parallel Language Integrated Query) と C# 4.0 (元のコードへの拡張あり/なし)
  • C、Windows API、スレッド、およびメモリ マップ ファイルを使用する、Windows ネイティブ コード
  • Windows C# と Microsoft .NET Framework によるマルチスレッド コード

これらすべてのソリューションのソース コードは、私の Web サイト (jmhartsoftware.com、英語) から入手できます。Windows Task Parallel Library (TPL: タスク並列ライブラリ) など、他の並列処理の技法については直接説明していませんが、PLINQ は TPL の上位に階層化されます。

各ソリューションの比較と評価

ソリューションの比較条件を、重要度順に示します。

  • タスクが完了するまでの処理時間に関する全体的パフォーマンス
  • 並列処理 (タスク数)、コア数、およびデータ コレクションのサイズのスケーラビリティ
  • コードの簡潔さ、正確さ、保守の容易性などの目に見えない要因

結果の概要

今回の記事では、代表的ベンチマークである検索の問題からの結果を示します。

  • データ処理の多くの問題でのパフォーマンスを向上するには、マルチコアの 64 ビット システムを有効に活用でき、PLINQ をソリューションに含めることができます。
  • 競争力のある、スケーラブルな PLINQ パフォーマンスを得るには、インデックス付きデータ コレクション オブジェクトが必要で、IEnumerable インターフェイスをサポートするだけでは不十分です。
  • C# または .NET とネイティブ コードを組み合わせたソリューションが最も高速です。
  • 最初の PLINQ ソリューションは、ほぼ 10 個の要因によって低速になり、3 つ以上にはタスクが拡張されません。一方、他のソリューションでは、6 コアを使用して最大 6 つのタスクまで適切に拡張されます (これはテストでの最大値です)。ただし、コードを強化すれば、最初のソリューションが大幅に改善されます。
  • PLINQ コードが最もシンプルで、あらゆる点で最も正確です。というのも、LINQ では、メモリ常駐データおよび外部データに対して、宣言型クエリ機能が提供されるためです。ネイティブ コードはわかりづらく、C# および .NET のコードは PLINQ コードよりもかなり優れてはいますが、PLINQ コードほどシンプルではありません。
  • 比較したすべての手法では、テスト システムの物理メモリの上限までファイル サイズが適切に拡張されます。

ベンチマーク問題: Geonames

今回の記事のアイデアは、Magennis 氏の LINQ に関する著書の第 9 章からの引用です。ここでは、725 万以上の地名が含まれる 825 MB のファイルから成る地理データベース (1,000 人につき 1 つ以上の場所) を検索することによって、PLINQ の使い方を例示しています。それぞれの地名は、UTF-8 (ja.wikipedia.org/wiki/UTF-8) のテキスト行レコード (可変長) と、タブで区切られる 15 個以上のデータ列で表されます。注: UTF-8 エンコードでは、タブ (0x9) または改行 (0xA) の値が、マルチバイト シーケンスの一部として出現しないことが保証されます。これは、さまざまな実装にとってきわめて重要です。

Magennis 氏の Geonames プログラムでは、ハードコーディングされたクエリを実装して、高度 (列 15) が 8,000 m を超えるすべての場所を特定し、それを降順に並べ替え、地名、国、および高度を表示します。ご参考までに、そのような場所は 16 か所存在し、エベレストが最も高い 8,848 m です。

Magennis 氏の報告では、処理時間が 22.3 秒 (1 コア) と 14.1 秒 (2 コア) でした。以前の経験 (私が執筆した「Windows 並列処理、高速ファイル検索と推論処理」(informit.com/articles/article.aspx?p=1606242、英語) など) では、この程度のサイズのファイルは数秒で処理でき、パフォーマンスはコア数に応じて適切に拡張されることが示されています。したがって、その経験を活かし、Magennis 氏の PLINQ コードを拡張してパフォーマンスを向上することにしました。最初の PLINQ の強化でパフォーマンスがほぼ倍になりましたが、スケーラビリティは向上しませんでした。ただし、それ以降の強化では、ネイティブ コードと C# マルチスレッド コードに匹敵するパフォーマンスが生み出されています。

次のような理由から、このベンチマークは興味深いと言えます。

  • 取り上げられているテーマ (地理上の場所と特性) 自体が興味深く、クエリの汎用化が容易です。
  • データの高度な並列処理が可能で、原則、すべてのレコードを並列処理できます。
  • 現在の標準と比べればファイルのサイズは控えめですが、Geonames の allCountries.txt ファイル自体を複数回連結するだけで、より大きなファイルを簡単にテストできます。
  • 処理がステートレスではありません。つまり、ファイルを分割するために行とフィールドの境界を特定しなければならず、個々のフィールドを特定するために行を処理する必要があります。

前提 1: すべてのバイトを調べる必要がある全体的な処理時間に比べて、並べ替えと表示にかかる時間がごく短くなるように、特定するレコード (この場合は、8,000 m を超える場所) の数が少ないことが前提です。

前提 2: パフォーマンスの結果が、メモリ常駐データのコレクションの処理に必要な時間を表すようにします。たとえば、プログラム手順の前半でデータを生成します。ベンチマーク プログラムはファイルを読み取りますが、ファイルが確実にメモリに常駐するように、テスト プログラムを複数回実行します。ただし、最初にファイルを読み込むのに必要な時間については、すべてのソリューションでほぼ同じです。

パフォーマンスの比較

最初のテスト システムは、6 コアのデスクトップ システムで、Windows 7 が実行されています (AMD Phenom II、2.80 GHz、4 GB RAM)。後で、ハイパースレッディング (HT) を使用し、それぞれコア数が異なる 3 つのシステムでの結果を示します (HT の詳細については、ja.wikipedia.org/wiki/ を参照してください)。

図 1 に、Geonames の 6 つの異なる ソリューションの結果を示します。この結果は、"Degree of Parallelism" (DoP: 並列処理の次数) の関数として処理時間 (秒) を表しています。DoP は並列タスクの数で、プロセッサ数よりも高く設定できます。テスト システムには 6 つのコアがありますが、実装によって DoP を制御しています。テスト システムは 6 つのタスクで最適に実行されるように調整されています。6 つ以上のタスクを使用すると、パフォーマンスは低下します。すべてのテストは、Geonames オリジナルの allCountries.txt ファイル (825 MB) を使用します。

image: Geonames Performance as a Function of Degree of Parallelism

図 1 DoP の関数としての Geonames のパフォーマンス

次にそれぞれ実装とその説明を示します。

  1. Geonames Original: Magennis 氏オリジナルの PLINQ ソリューションです。パフォーマンスが低く、プロセッサ数に応じて拡張されません。
  2. Geonames Helper: Geonames Original のパフォーマンスを強化したバージョンです。
  3. Geonames MMChar: Geonames Threads と同じようなメモリ マップ ファイル クラスを使って Geonames Helperの強化を試みましたが、うまくいかなかったソリューションです。注: メモリ マップを使用すると、明示的な I/O 操作を行わずに、メモリ内に存在するかのようにファイルを参照できます。これにより、パフォーマンスを向上できます。
  4. Geonames MMByte: このソリューションは、入力ファイルの各バイトを処理するように MMChar を変更します。上記の 3 つのソリューションは、UTF-8 文字列を Unicode (1 文字あたり 2 バイト) に変換します。パフォーマンスは最初の 4 つのソリューションの中で最高で、Geonames Original のパフォーマンスの倍以上です。
  5. Geonames Threads: PLINQ を使用しません。これは、スレッドとメモリ マップ ファイルを使用する、C# と .NET による実装です。パフォーマンスは Index (次に説明するソリューション) よりも速く、Native と同程度です。このソリューションと Geonames Native では、並列処理の最高のスケーラビリティが実現されます。
  6. Geonames Index: この PLINQ ソリューションは、データ ファイルを前処理 (約 9 秒かかります) し、その後の PLINQ 処理のために、メモリ常駐の List<byte[]> オブジェクトを作成します。処理コストを複数のクエリで償却できるため、パフォーマンスは Geonames Native と Geonames Threads よりもほんのわずかに遅いだけです。
  7. Geonames Native: (図 1 には示されていません) PLINQ を使用しません。これは、スレッドとメモリ マップ ファイルを使用する、C 言語による Windows API の実装です。これは、私の著書『Windows System Programming』(Addison-Wesley、2010 年) の第 10 章に記載しています。これらの結果を得るには、コンパイラの完全最適化がきわめて重要です。既定の最適化では、約半分ほどのパフォーマンスしか実現されません。

すべての実装は、64 ビット システムでビルドしたものです。32 ビット システムでビルドしてもほとんどの場合はうまくいきますが、サイズの大きなファイルではうまくいきません (図 2 参照)。図 2 に、DoP が 4 で、ファイルのサイズを大きくした場合のパフォーマンスを示します。

image: Geonames Performance as a Function of File Size

図 2 ファイル サイズの関数としての Geonames のパフォーマンス

この場合、テスト システムには 4 コアが搭載されています (AMD Phenom クアッド コア、2.40 GHz、8GB RAM)。サイズの大きいファイルは、オリジナルのファイルの複数のコピーを連結して作成しています。図 2 には、Geonames Index を含む、3 つの最も高速なソリューションのみを表示しています。Geonames Index は、(ファイルの処理は抜きにして) 最も高速な PLINQ ソリューションで、パフォーマンスは物理メモリの上限までファイル サイズに応じて拡張されます。

ここで、2 ~ 7 の実装について説明し、PLINQ 技法について詳しく説明します。その後、他のテスト システムの結果を説明し、発見した結果をまとめます。

PLINQ の強化ソリューション: Geonames Helper

図 3 に、Geonames Original コードに変更 (太字部分) を加えた、Geonames Helper を示します。

図 3 オリジナルの PLINQ コードに加えた変更を強調表示した Geonames Helper

class Program
{
  static void Main(string[] args)
  {
    const int nameColumn = 1;
    const int countryColumn = 8;
    const int elevationColumn = 15;

    String inFile = "Data/AllCountries.txt";
    if (args.Length >= 1) inFile = args[0];
        
    int degreeOfParallelism = 1;
    if (args.Length >= 2) degreeOfParallelism = int.Parse(args[1]);
    Console.WriteLine("Geographical data file: {0}. 
      Degree of Parallelism: {1}.", inFile, degreeOfParallelism);

    var lines = File.ReadLines(Path.Combine(
      Environment.CurrentDirectory, inFile));

    var q = from line in 
      lines.AsParallel().WithDegreeOfParallelism(degreeOfParallelism)
        let elevation = 
          Helper.ExtractIntegerField(line, elevationColumn)
        where elevation > 8000 // elevation in meters
        orderby elevation descending
        select new
        {
          elevation = elevation,
          thisLine = line
         };

    foreach (var x in q)
    {
      if (x != null)
      {
        String[] fields = x.thisLine.Split(new char[] { '\t' });
        Console.WriteLine("{0} ({1}m) - located in {2}",
          fields[nameColumn], fields[elevationColumn], 
          fields[countryColumn]);
      }
    }
  }
}

多くの読者が PLINQ と C# 4.0 に詳しくないかもしれないため、強化内容も含めて、図 3 について説明します。

  • 10 ~ 15 行目は、ユーザーが入力ファイル名と、並列処理の次数 (同時実行タスクの最大数) を、コマンド ラインから指定できるようにしています。オリジナルのコードではハードコーディングされていました。
  • 17 ~ 18 行目は、ファイル内の行の非同期読み取りを開始します。lines の型は C# の String 型の配列として暗黙のうちに設定されます。lines 値は、20 ~ 28 行目までは使用されません。Geonames MMByte などの他のソリューションでは、異なるクラスと独自の ReadLines メソッドを使用するため、変更する必要がある行はこれらのコード行のみです。
  • 20 ~ 28 行目は、LINQ コードと、PLINQ の AsParallel 拡張機能です。このコードは SQL に似ており、"q" 変数は、整数値の高度と文字列から構成されるオブジェクトの配列として、暗黙のうちに型指定されます。PLINQ がすべてのスレッド管理作業を実行します。AsParallel メソッドはシリアル処理される LINQ コードを PLINQ コードに変更するためだけに必要です。
  • 23 行目は、図 4 に示す Helper.ExtractIntegerField メソッドです。オリジナルのプログラムでは、36 行目で結果を表示するために使用したのと同じような方法で、String.Split メソッドを使用しています (図 3 参照)。Geonames Original と比べて Geonames Helper のパフォーマンスが向上している重要な点は、すべての行のすべてのフィールドに、String オブジェクトを割り当てる必要がなくなることにあります。

図 4 Geonames の Helper クラスと ExtractIntegerField メソッド

class Helper
{
  public static int ExtractIntegerField(String line, int fieldNumber)
  {
    int value = 0, iField = 0;
    byte digit;

    // Skip to the specified field number and extract the decimal value.
    foreach (char ch in line)
    {
      if (ch == '\t') { iField++; if (iField > fieldNumber) break; }
      else
      {
        if (iField == fieldNumber)
        {
          digit = (byte)(ch - 0x30);  // 0x30 is the character '0'
          if (digit >= 0 && digit <= 9) 
            { value = 10 * value + digit; }
          else // Character not in [0-9]. Reset the value and quit.
          { value = 0; break; }
        }
      }
    }
    return value;
  }
}

図 3 の 21 行目で使用されている AsParallel メソッドは、すべての IEnumerable オブジェクトと併用できます。前述のように、図 4 は、Helper クラスの ExtractIntegerField メソッドを示しています。これは単に、パフォーマンスを向上するためにライブラリ メソッドを使用しないように、指定されたフィールド (この場合は elevation) を抽出して評価しています。図 1 では、この強化によって DoP 1 のパフォーマンスが倍になっているのがわかります。

Geonames MMChar と Geonames MMByte

Geonames MMChar は、FileMmChar というカスタム クラスを使用して、入力ファイルをメモリにマップすることによりパフォーマンスを向上しようとして、うまくいかなかったものです。ただし、Geonames MMByte は、入力ファイルのバイト列を Unicode に変換しないという大きなメリットがあります。

MMChar には、IEnumerable<String> インターフェイスをサポートする、FileMmChar という新しいクラスが必要です。FileMmByte クラスも同様で、文字列オブジェクトではなく、byte[] オブジェクトを処理します。唯一の大幅なコード変更は、図 3 の 17 ~ 18 行目で、以下のようにします。

var lines = FileMmByte.ReadLines(Path.Combine(
    Environment.CurrentDirectory, inFile));

次のコードをご覧ください。

public static IEnumerable<byte[]> ReadLines(String path)

このコードでは、FileMmByte で IEnumerable<byte[]> インターフェイスをサポートし、FileMmByte オブジェクトと IEnumerator<byte[]> オブジェクトを構築して、各行にマップされたファイルをスキャンします。

FileMmChar クラスと FileMmByte クラスは、ファイルにアクセスするためにポインターを作成して使用するため "安全ではなく"、C# コードおよびネイティブ コードの相互運用性を使用することに注意してください。ただし、すべてのポインターの使用は独立したアセンブリ内に分離され、コードではポインターの逆参照ではなく配列を使用します。.NET Framework 4 の MemoryMappedFile クラスは、マップされたメモリからデータを移動するためにアクセサー関数を使用する必要があるため役に立ちません。

Geonames Native

Geonames Native では、Windows API、スレッド、およびメモリ マップ ファイルを使用します。基本コード パターンについては、『Windows System Programming』の第 10 章で説明しています。プログラムでは、スレッドを直接管理しなければならず、ファイルをメモリに慎重にマップする必要もあります。パフォーマンスは、Geonames Index を除くすべての PLINQ 実装よりはるかに優れています。

ただし、Geonames の問題と、単純でステートレスなファイル検索または変換の間には、重要な違いがあります。入力データをパーティション分割して、異なるパーティションを異なるタスクに割り当てるための適切な方法を決めることが課題となります。すべてのファイルをスキャンすることなく行の境界を判断する明快な方法がないため、固定サイズのパーティションを各タスクに割り当てることは現実的ではありません。ただし、次のように DoP 4 で説明すると、このソリューションはわかりやすくなります。

  • 入力ファイルを 4 つの均等なパーティションに分割します。パーティションの開始位置は、スレッド関数の引数の一部として、各スレッドに伝えます。
  • 次に、パーティション内で "開始" されるすべての行 (レコード) は、各スレッドで処理します。つまり、そのパーティションで開始される最終行の処理を完了するため、スレッドはおそらく次のパーティションをスキャンすることになります。

Geonames Threads

Geonames Threads では、Geonames Native と同じロジックを使用します。実際に、一部のコードはまったく同じか、ほぼ同じです。ただし、ラムダ式、拡張メソッド、コンテナー、およびその他の C# と .NET の機能により、コーディングがはるかに簡素化されます。

MMByte と MMChar と同様、メモリ マップ ファイルでは、マップされたメモリへのポインターを使用するために、"安全ではない" クラスと、C# コードとネイティブ コードの相互運用性が必要になります。ただし、Geonames Threads は、Geonames Native よりもはるかに単純なコードを使用しているのにパフォーマンスが変わらないため、これに取り組む価値はあります。

Geonames Index

PLINQ の結果 (Original、Helper、MMChar、および MMByte) は、Native および .NET Threads の結果と比較すると、期待はずれかもしれません。パフォーマンスを犠牲にすることなく、PLINQ の簡潔さと正確さを活用する方法はあるのでしょうか。

PLINQ でクエリ (図 3 の 20 ~ 30 行目) が処理される正確な方法を判断することはできませんが、おそらく PLINQ には、独立した複数のタスクで並列処理を行うために、入力行をパーティション分割する優れた方法がありません。作業の仮説として、パーティション分割が PLINQ のパフォーマンス上の問題の原因である可能性を考えてみましょう。

Magennis 氏の書籍 (276 ~279 ページ) では、行の String 型の配列は IEnumerable<String> インターフェイスをサポートしています (John Sharp 氏の書籍『Microsoft Visual C# 2010 Step by Step』(Microsoft Press、2010 年) の第 19 章も参照してください)。しかし、行のインデックスは設定していないため、PLINQ ではおそらく "チャンク パーティション分割" が使用されます。また、FileMmChar クラスと FileMmByte クラスの IEnumerator.MoveNext メソッドでは、次の新しい行が見つかるまですべての文字をスキャンする必要があるため、低速になります。

行の String 型の配列にインデックスを設定したらどうなるでしょう。特に、入力ファイルをメモリにマップしてこれを補完すると、PLINQ のパフォーマンスが向上するのではないでしょうか。Geonames Index では、この技法を使えばパフォーマンスが向上し、ネイティブ コードに匹敵する結果がもたらされたことを示しています。ただし、一般的に、行をメモリ内リストかメモリ内配列のいずれかに移動するには事前のコストがかかります。メモリ内リストやメモリ内配列にインデックスを設定する (コストは複数のクエリを使えば償却できます) か、おそらくプログラム手順の前段階でファイルまたは他のデータ ソースを処理する際に事前にインデックスを設定して、処理コストを削減します。

事前にインデックスを設定する操作は簡単です。各行に 1 行ずつにアクセスして、リストに行を追加するだけです。図 3 の 17 ~ 18 行目と、このコード スニペットでは、リスト オブジェクトを使用します。ここでは次のような前処理を行います。

// Preprocess the file to create a list of byte[] lines
List<byte[]> lineListByte = new List<byte[]>();
var lines = 
    FileMmByte.ReadLines(Path.Combine(Environment.CurrentDirectory, inFile));
// ... Multiple queries can use lineListByte
// ....
foreach (byte[] line in lines) { lineListByte.Add(line); }
// ....
var q = from line in lineListByte.AsParallel().
    WithDegreeOfParallelism(degreeOfParallelism)

リストを配列に変換してデータをさらに処理することにより若干効率が上がりますが、これにより前処理にかかる時間が増加します。

最終的なパフォーマンス強化

ExtractIntegerField メソッドで行のすべての文字をスキャンして、指定したフィールドに書き出す必要がなくなるように、各行のフィールドにインデックスを設定すれば、Geonames Index のパフォーマンスはさらに向上します。

Geonames IndexFields の実装では、ReadLines メソッドから返される行が各フィールドの場所を保持する byte[] 配列と uint[] 配列の両方を含むオブジェクトになるように、ReadLines メソッドを変更します。これにより、Geonames Index でパフォーマンスが約 33% 向上し、ネイティブ コードと C#/.NET によるソリューションにかなり近づきます。(Geonames IndexFields は、コード ダウンロードに含めてあります)。また、各フィールドをすぐに使用できるため、汎用のクエリをはるかに簡単に構築できるようになります。

制限事項

効率のよいすべてのソリューションではメモリ常駐データが必要になるため、非常にサイズの大きいデータ コレクションにはパフォーマンス上のメリットは生じません。この場合の "非常にサイズの大きい" とは、システムの物理メモリ サイズに近いデータ サイズを指します。Geonames の例では、3,302 MB のファイル (オリジナル ファイルの 4 つのコピー) は 8 GB のテスト システムで処理できます。ただし、ファイルを 8 つ連結したコピーでテストすると、すべてのソリューションで処理速度が非常に低速になりました。

既に説明したように、データ ファイルが最近アクセスされてメモリ内にある可能性が高いという意味では、データ ファイルが "処理中" であればパフォーマンスが最高になります。初回実行時にデータ ファイルでページ切り替えが発生するとこれに 10 秒以上かかる場合があり、これは先ほどのコード スニペットのインデックス作成処理に匹敵します。

つまり、ここでの結果は、メモリ常駐データの構造に当てはまります。現在のメモリ サイズと価格では、かなりのデータ オブジェクト (725 万個の地名が含まれるファイルなど) をメモリ常駐にできます。

その他のテスト システムの結果

図 5 に、他のシステム (Intel i7 860、2.80 GHz、4 コア、8 スレッド、Windows 7、4GB RAM) でのテスト結果を示します。このプロセッサではハイパースレッディングがサポートされるため、テストした DoP の値は 1 ~ 8 です。図 1 は、6 コアの AMD テスト システムに基づいていますが、このシステムではハイパースレッディングがサポートされていません。

image: Intel i7 860, 2.80GHz, Four Cores, Eight Threads, Windows 7, 4GB RAM

図 5 Intel i7 860、2.80 GHz、4 コア、8 スレッド、Windows 7、4GB RAM

以下に示す 2 つの追加のテスト構成でも、同様の結果が生成されました (この完全なデータは私の Web サイトから入手できます)。

  • Intel i7 720、1.60 GHz、4 コア、8 スレッド、Windows 7、8GB RAM
  • Intel i3 530、2.93 GHz、2 コア、4 スレッド、Windows XP (64 ビット版)、4GB RAM

パフォーマンスには、以下のような興味深い特徴があります。

  • Geonames Threads は、Geonames Native と並んで、最高のパフォーマンスが一貫して提供されます。
  • Geonames Index は、最も高速な PLINQ ソリューションで、Geonames Threads のパフォーマンスに匹敵します。注: Geonames IndexFields の方がわずかに高速ですが、図 5 には示されていません。
  • Geonames Index 以外は、DoP が 2 より大きい場合、すべての PLINQ ソリューションが DoP に応じて効率が下がります。つまり、並列タスクの数が増加するにつれて、パフォーマンスが低下します。この例から、PLINQ では、インデックスを設定したオブジェクトを使用する場合のみパフォーマンスが向上します。
  • ハイパースレッディングによるパフォーマンスへの貢献はわずかです。したがって、DoP が 4 を超えると、Geonames Threads と Geonames Index のパフォーマンスは大幅には向上しません。このように HT のスケーラビリティが乏しいのは、可能な限り異なるコアで実行するようにしたからではなく、同じコアの論理プロセッサに 2 つのスレッドのスケジュールを設定した結果かもしれません。ただし、Mark E. Russinovich 氏、David A. Solomon 氏、および Alex Ionescu 氏は、その共著『Windows Internals, Fifth Edition』(Microsoft Press、2009 年) の 40 ページで、物理プロセッサは論理プロセッサより前にスケジュールが設定されると書いていますが、先ほどの説明は彼らが言うほどもっともらしくは思われません。HT を使用しない AMD システム (図 1) では、Threads、Native、および Index のシーケンシャルな DoP が 1 つの場合の結果と比較して、DoP が 4 より大きい場合にパフォーマンスが 3 ~ 4 倍になっていました。図 1 は、DoP がコア数と同じ場合に最高のパフォーマンスが実現されることを示しています。この場合、マルチスレッドのパフォーマンスは、DoP が 1 の場合のパフォーマンスの 4.2 倍になります。

結果のまとめ

PLINQ では、メモリ内データ構造を処理する優れたモデルが提供され、いくつかの単純な変更 (Helper など) や、MMByte で示したようなより高度な技法を使用して、既存のコードのパフォーマンスを向上できます。ただし、簡単な強化では、ネイティブ コードやマルチスレッドの C#/.NET コードに近いパフォーマンスは実現されません。また、機能強化は、コア数と DoP に応じて拡張されるわけではありません。

PLINQ では、ネイティブ コードと C#/.NET コードによるパフォーマンスに近づくことはできますが、インデックスを設定したデータ オブジェクトを使用する必要があります。

コードとデータの使用

すべてのコードは、私の Web サイト (jmhartsoftware.com/SequentialFileProcessingSupport.html、英語) から入手できます。以下の手順に従ってください。

  • ページに移動して、PLINQ コードと Geonames Threads コードが含まれる ZIP ファイルをダウンロードします。すべての PLINQ のバリエーションは、GeonamesPLINQ プロジェクト内にあります (これは Visual Studio 2010 プロジェクトですが、Visual Studio 2010 Express でも事足ります)。Geonames Threads は、GeonamesThreads という Visual Studio 2010 プロジェクト内にあります。これらのプロジェクトは、いずれも 64 ビットのリリース ビルド向けに構成されています。この ZIP ファイルには、図 1図 2、および図 5 で使用したそのままのパフォーマンス データを記載した Excel ブックも含めてあります。このファイルの先頭にある簡単な "Usage" (使用方法) コメントでは、入力ファイル、DoP、および実装を選択するためのコマンド ライン オプションについて説明しています。
  • 『Windows System Programming』のサポート ページ (jmhartsoftware.com/comments_updates.html、英語) に移動して、Geonames Native コードとプロジェクト (ZIP ファイル) をダウンロードしてください。Geonames の複数のプロジェクトは ZIP ファイル内にあります。ReadMe.txt ファイルには、構造に関する説明があります。
  • GeoNames データベース (download.geonames.org/export/dump/allCountries.zip、英語) をダウンロードします。

確認していない問題

今回は、同じ問題を解決する複数の技法のパフォーマンスを比較しました。このアプローチは、ここで説明したような標準インターフェイスを使用し、プロセッサとスレッドに合わせて単純な共有メモリ モデルを想定しています。ただし、テスト コンピューターの基になる実装や特定の機能についてはあまり詳しく調べていないため、今後はさまざまな疑問点について詳しく説明する可能性があります。次にいくつか例を示します。

  • キャッシュ ミスの影響と、その影響を軽減する方法。
  • ソリッド ステート ディスクの影響。
  • PLINQ の Index、Threads、および Native の各ソリューション間のパフォーマンスの差異を少なくする方法。FileMmByte IEnumerator の MoveNext メソッドと Current メソッドでコピーするデータ量を減少させる試みでは、大きなメリットは得られませんでした。
  • メモリ帯域幅、CPU 速度、およびその他のアーキテクチャ上の機能によって決まる理論上の最大値に、パフォーマンスは近づくか。
  • HT システム ( 図 5) でのパフォーマンスを、HT を使用しないシステム (図 1) に匹敵するパフォーマンスに拡張可能にする方法。
  • プロファイリング ツールや Visual Studio 2010 ツールを使用して、パフォーマンスのボトルネックを特定および削除できるかどうか。

読者の皆さんがこれらについてさらに究明できることを期待します。

Johnson (John) M. Hart は、Microsoft Windows および Microsoft .NET Framework アプリケーションのアーキテクチャと開発、技術トレーニング、および執筆を専門にしているコンサルタントです。Cilk Arts Inc. (Intel Corp. による買収後)、Sierra Atlantic Inc.、Hewlett-Packard Co.、および Apollo Computer で、ソフトウェア エンジニア、エンジニアリング マネージャー、およびアーキテクトとしての長年にわたる経験があります。長年コンピューター サイエンスの教授を務め、『Windows System Programming』(Addison-Wesley、2010 年) の第 4 版を執筆しました。

この記事のレビューに協力してくれた技術スタッフの Michael BruestleAndrew GreenwaldTroy Magennis、および CK Park に心より感謝いたします。