kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

ToList / ToArray の性能 – GC 特性とCLRメモリヒューリスティックス


neuecc さんの ToArray vs ToList を受けてちょっと書いてみます。

というのも、 .NET Framework にはパフォーマンス上ある一点に断崖絶壁があり、この断崖絶壁を何の気なしに超えてしまうと一気にパフォーマンスが落ちます。この断崖絶壁すなわちオブジェクトのサイズによる CLR のヒューリスティックが発動される時です。

例えばこのヒューリスティックはオブジェクトのサイズが 24KB を超える点で発動します。

CLR2 ではこのヒューリスティックは LOH へのオブジェクトの配置、CLR4では Gen1 に確保する様になっているようです。

このヒューリスティックの発動は性能面ではっきりした影響を表す事に注意してください、最も単純な例でも 20% ~ 30% の性能影響がでます。 (参考: .NET CLR2 と CLR4 の StringBuilder のパフォーマンス )

 

確認してみましょう。

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

namespace ToArrayPerformance
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch stopWatch = new Stopwatch();
            for (int elmCount = 3500; elmCount < 8000; elmCount += 100)
            {
                stopWatch.Restart();
                for (int i = 0; i < 100000000/elmCount; i++)
                {
                    int[] result = Enumerable.Range(1,elmCount).ToArray();
                }
                stopWatch.Stop();
                Console.WriteLine( DateTime.Now.TimeOfDay +"\t"+
                    elmCount.ToString() + "\t" + 
                    stopWatch.Elapsed );
            }
            Console.ReadLine();
        }
    }
}

単純に Enumerable.Range を (1,n) する事の n を 3500 –> 8000 まで100 づつ伸ばしています。ループ回数を十分に大きく、しかし nに逆比例させる事で計測期間内で処理する要素数は平均的には一緒です。

09:33:24.0662034        3500    00:00:03.6627269

09:33:27.4823988        3600    00:00:03.4151080

09:33:31.2646151        3700    00:00:03.7812066

09:33:33.9517688        3800    00:00:02.6867959

09:33:37.1399512        3900    00:00:03.1877383

09:33:40.7921601        4000    00:00:03.6520175

09:33:44.7573869        4100    00:00:03.9653936

09:33:48.3675933        4200    00:00:03.6096440

09:33:52.6018355        4300    00:00:04.2337512

09:33:56.5520615        4400    00:00:03.9497644

09:33:59.6342378        4500    00:00:03.0817188

09:34:03.1714401        4600    00:00:03.5373395

09:34:07.0466617        4700    00:00:03.8745989

09:34:10.2968476        4800    00:00:03.2493948

09:34:13.9820584        4900    00:00:03.6850011

09:34:17.8952822        5000    00:00:03.9133208

09:34:21.6224954        5100    00:00:03.7266563

09:34:24.9926882        5200    00:00:03.3696358

09:34:28.5378910        5300    00:00:03.5445853

09:34:31.8280791        5400    00:00:03.2904550

09:34:35.7383028        5500    00:00:03.9096186

09:34:38.5334627        5600    00:00:02.7946198

09:34:41.1226108        5700    00:00:02.5892790

09:34:44.6508126        5800    00:00:03.5274057

09:34:47.9480011        5900    00:00:03.2966803

09:34:52.0562361        6000    00:00:04.1073346

09:34:56.2544763        6100    00:00:04.1981550

09:35:00.3177087        6200    00:00:04.0629688

09:35:04.5789524        6300    00:00:04.2609572

09:35:08.7101887        6400    00:00:04.1306676

09:35:12.1133833        6500    00:00:03.4019436

09:35:16.5896394        6600    00:00:04.4765241

09:35:20.6798733        6700    00:00:04.0892036

09:35:25.0381226        6800    00:00:04.3580106

09:35:29.1703589        6900    00:00:04.1319132

09:35:33.1835885        7000    00:00:04.0123602

09:35:37.2978238        7100    00:00:04.1144444

09:35:41.1280429        7200    00:00:03.8297086

09:35:44.0262086        7300    00:00:02.8976320

09:35:46.4393467        7400    00:00:02.4127087

09:35:50.6465873        7500    00:00:04.2066884

09:35:54.7918244        7600    00:00:04.1448968

09:35:58.6760466        7700    00:00:03.8837421

09:36:03.0412962        7800    00:00:04.3644022

09:36:06.3874876        7900    00:00:03.3454986

3要素目の処理時間を見ると2つのトレンドがある事に気づいてもらえますでしょうか。

6000要素を超えたとたんに 4秒以上処理時間を要する様に変わっています。

コレのパフォーマンスグラフを見ると以下の様になります。

image

端的には %Time in GC ががくっと上がっているのが目に見えて解ります。それを起点として立ち上がってるカウンターは # Gen1 Collections です。

image

int で 各4バイトの6000要素ですので、24KB がこの性能の境界線である事が解ります。

要するにアプリケーションが確保する領域のサイズが頻繁にこの境界を超えると3割ぐらい性能が落ちるという事になりますね。

 

さて、性能狂になる準備は整ったも同然ですね、練習問題です。

  1. 24KBを超えない様に内部バッファを管理してコピーを実行する GcNiYasasiiToArray を実装しましょう。
  2. この GcNiYasasiiToArray が管理する 24KB 以下に制約された中間バッファは当然に相当数のインスタンスが生成される事になります。(大きなデータを扱った場合、同じ24KB程度のサイズのバッファがいくつも作られる)これを再利用する事の有用性と馬鹿馬鹿しさを論じた上で実際に試して有用さ無駄さを評価してください。
    1. 再利用される事によりオブジェクトが長寿命になるという事は24KB以下のオブジェクトは Gen0 に作られるという点に配慮した意味を失うかもしれません。これがパフォーマンスグラフにどう表れるか、性能にどう影響を与えたかを評価しなければなりません。
    2. 配列は確保時にゼロクリアされます、このゼロクリアは要素を上書きするのであれば無駄なことで、これが避けられる様になる事は性能に影響する可能性があります、この性能変化を評価してください。
  3. あなたのアプリケーションに GCへの優しさを意識させると性能が変わりますか?それをパフォーマンスグラフより判断し、改善する事が期待できる場合、改善するべきポイントを CLR Profiler によって発見してください。

LOH は 85KB かららしいですね、この境界でのパフォーマンスグラフの違いを見るというのも練習問題としていいかもしれません。

 

なーんて、練習問題でお茶を濁す教科書風な終わり方をしてみます。Gen1 GCだけで3割性能が変わるわけで、Gen2 のFull GCとか気絶しちゃうぐらい怖い話なんでGCには優しく、女の子にも優しくしましょうね。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。