19

次の C# コードは、VS2008 よりも VS2010 でビルドすると実行速度が遅くなるようです: Core i5 Win7 x64 8 GB RAM PC では、VS2008 でビルドされたバージョンは約 7.5 秒で文字列をソートしますが、VS2010 でビルドされたバージョンでは約 9 秒かかります。何故ですか?

私のコードに何か問題がありますか?

VS2010 で並べ替えアルゴリズムが変更されましたか?

基になる CLR に、パフォーマンスを低下させる何か違いはありますか?

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;

namespace StringSortCSharp
{
    /// <summary>
    /// Console app to test string sorting performance in C#.
    /// </summary>
    class Program
    {
        /// <summary>
        /// Displays the first lines from a vector of strings.
        /// </summary>
        /// <param name="wishedN">Number of lines to display.</param>
        /// <param name="lines">Source lines to display.</param>
        private static void DisplayFirst(int wishedN, List<string> lines)
        {
            int n = Math.Min(wishedN, lines.Count);
            for (int i = 0; i < n; i++)
            {
                Console.WriteLine("  " + lines[i]);
            }
            Console.WriteLine();
        }

        /// <summary>
        /// Used for random permutation.
        /// </summary>
        private static Random random = new Random();

        /// <summary>
        /// Computes a random permutation of the input sequence.
        /// 
        /// From:
        ///     http://stackoverflow.com/questions/375351/most-efficient-way-to-randomly-sort-shuffle-a-list-of-integers-in-c-sharp
        /// 
        /// </summary>
        /// <typeparam name="T">Type stored in the sequences.</typeparam>
        /// <param name="sequence">Input sequence.</param>
        /// <returns>Random permutation of the input sequence.</returns>
        private static IEnumerable<T> RandomPermutation<T>(IEnumerable<T> sequence)
        {
            T[] retArray = sequence.ToArray();


            for (int i = 0; i < retArray.Length - 1; i += 1)
            {
                int swapIndex = random.Next(i + 1, retArray.Length);
                T temp = retArray[i];
                retArray[i] = retArray[swapIndex];
                retArray[swapIndex] = temp;
            }
            return retArray;
        }


        /// <summary>
        /// Builds a list of strings used in the performance benchmark.
        /// </summary>
        /// <returns>Test list of strings.</returns>
        private static List<string> BuildTestLines()
        {
            // Start with "Lorem ipsum", and repeat it several times, adding some suffix strings.

            var lorem = new string[]
             {
                 "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",
                 "Maecenas porttitor congue massa. Fusce posuere, magna sed",
                 "pulvinar ultricies, purus lectus malesuada libero,",
                 "sit amet commodo magna eros quis urna.",
                 "Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.",
                 "Pellentesque habitant morbi tristique senectus et netus et",
                 "malesuada fames ac turpis egestas. Proin pharetra nonummy pede.",
                 "Mauris et orci."
             };

            int repeatCount = 200 * 1000;

            Console.Write("Building test strings");
            var testLines = new List<string>();

            Console.Write(" (total string count = {0})", repeatCount * lorem.Length);
            Console.Write("...");
            for (int i = 0; i < repeatCount; i++)
            {
                for (int j = 0; j < lorem.Length; j++)
                {
                    // Add more stuff to Lorem strings 
                    testLines.Add(lorem[j] + " (#" + i + ")");
                }
            }
            Console.WriteLine("done.");

            DisplayFirst(5, testLines);
            Console.WriteLine();

            // Shuffle the previously built strings.

            Console.Write("Shuffling strings...");
            var randomLines = new List<string>(RandomPermutation(testLines));
            Console.WriteLine("done.");
            DisplayFirst(5, randomLines);
            Console.WriteLine();

            return randomLines;
        }


        /// <summary>
        /// Sort the input lines.
        /// </summary>
        /// <param name="lines">Input lines to sort.</param>
        private static void Test(List<string> lines)
        {
            // Stopwatch to measure time performance
            var timer = new Stopwatch();

            Console.Write("Sorting " + lines.Count + " lines...");

            // Sort benchmark

            timer.Start();
            lines.Sort();
            timer.Stop();
            Console.WriteLine("done.");

            // Display results

            DisplayFirst(5, lines);

            Console.WriteLine();
            Console.WriteLine((timer.ElapsedMilliseconds / 1000.0).ToString(CultureInfo.InvariantCulture) + " seconds elapsed.");
        }

        static void Main(string[] args)
        {
            Console.WriteLine("*** Testing String Sorting in C# ***");
            Console.WriteLine();

            // Build test lines used for the sort benchmark
            List<string> testLines = BuildTestLines();

            // Run the sort test
            Test(testLines);
        }
    }
}
4

1 に答える 1

28

.NET バージョンで使用される並べ替えアルゴリズムの概要を次に示します。List<T>.Sort()内部で使用することを覚えておくと役立ちますArray<T>.Sort()

  • .NET 2.0 ~ 4.0 では、クイック ソート アルゴリズムを使用してArray. コードに若干の変更が加えられていますが、ほとんどのコードは同じままです。
  • .NET 4.5 では、配列の並べ替えアルゴリズムがクイック 並べ替えから内省的並べ替えに変更されました。これは以前よりも大きな変更であり、少なくとも私のテストでは、かなりのパフォーマンスの向上が見られます。

VS2010 で並べ替えアルゴリズムが変更されましたか?

はい。ただし、変更はマイナーなものであり、パフォーマンスには影響しません。2000 万のシャッフルされた整数1に対する並べ替えを考えてみましょう。

List<int>.Sort() (2000 万)

.NET 3.5 .NET 4.0 .NET 4.5
--------- --------- ---------
 2.564秒 2.565秒 2.337秒

パフォーマンスに関しては、v3.5 と v4.0 の間で変更はありません。v4.5 では速度が大幅に向上しています。違いを生み出しているのは、実際の並べ替えアルゴリズムではないことは明らかです。

次の質問に移る前に、私のマシンで実際のコードを実行した結果を共有させてください。

List<string>.Sort() (160 万)

.NET 3.5 .NET 4.0 .NET 4.5
--------- --------- ---------
 7.953秒 11.267秒 10.092秒

あなたと同じような結果が得られます。これらの結果は、次の質問への良いリードインです。

基になる CLR に、パフォーマンスを低下させる何か違いはありますか?

間違いなく。それで、違いは何ですか?違いは、文字列比較の実装にあります。ソート アルゴリズムの各ステップで 2 つの文字列を比較する必要がありますが、v2.0 ランタイムと v4.0 ランタイムでは異なる方法で行われます。(以下の追加の注意事項を参照してください)

これを証明する最も簡単な方法は、カルチャに依存するのではなく、序数による並べ替えを強制することです。に置き換えlines.Sort();ますlines.Sort(StringComparer.Ordinal);。これが私が測定したものです:

List<string>.Sort(StringComparer.Ordinal) (160 万)

.NET 3.5 .NET 4.0 .NET 4.5
--------- --------- ---------
 4.088秒 3.76秒 3.454秒

これで、見栄えが良くなりました!それは多かれ少なかれ私が期待したものです。リリースされたフレームワークのバージョンごとに速度が着実に向上しています。 MSDNは、文字列に対して言語以外の比較を行う場合は、序数比較を使用することを提案しています。

ただし、比較または並べ替えがカルチャに依存しない場合にのみ、問題が解決します。カルチャに依存した並べ替えが必要な場合は、.NET 3.5 フレームワークに戻さない限り、実行時間の遅さを解消できないようです。


特記事項

List<T>.Sort()またはに比較子を渡さない場合Array.Sort、デフォルトの比較子が使用されます。.NET 文字列の既定の比較子は、スレッドの現在のカルチャの比較子を使用します。そこから、.NET ランタイム ネイティブ ライブラリのいくつかの内部関数を呼び出します。

v2.0-3.5 では、COMNlsInfo::Compareand を呼び出しますCOMNlsInfo::CompareFast。コールスタック(ちょっと)は次のようになります。

String.CompareTo(文字列)
+--System.Globalization.CompareInfo.Compare(string,string,CompareOptions)
   +--mscorwks.dll!COMNlsInfo::比較
      +--mscorwks.dll!COMNlsInfo::CompareFast

これらの関数の同様のソースは、CLI (SSCLI) の共有ソース実装に表示されます。sscli\clr\src\classlibnative\nls\comnlsinfo.cppそれぞれ1034号線と893号線にあります。

ただし、v4.0 では、その呼び出しツリーがかなり大幅に変更されました。

String.CompareTo(文字列)
+--System.Globalization.CompareInfo.Compare(string,string,CompareOptions)
   +--clr.dll!COMNlsInfo::InternalCompareString
      +--clr.dll!SortVersioning::SortDllCompareString
         +--nlssorting.dll!_SortCompareString
            +--nlssorting.dll!_AsciiCompareString

なぜ一方が他方より遅いのかを教えていただければ幸いですが、まったく手がかりがなく、.NET 4.0 と比較する SSCLI もありません。.NET 4.0 での文字列処理の主要な変更には、問題がなかったわけではありません。.NET 4.0 の文字列に関連するパフォーマンスの問題がありましたが、ここでは実際には当てはまりません。


1すべてのテストは仮想マシンで実行されました。Win 2008R2 x64 w/ 4GB RAM および仮想クアッドコア プロセッサ。ホスト マシンは Win7 x64、24GB RAM および Xeon W3540 (2.93ghz) クアッドコア (8 論理プロセッサ) です。結果は、最高の時間と最悪の時間を除いた 5 回の実行の平均です。

于 2012-08-29T18:13:04.010 に答える