トップ回答者
chartコントロールの描画を速くしたい

質問
-
いつもお世話になっております
VS2008 でC++/CLIを使ってフォームアプリを作成しています下記のMicrosoft Chart Controlsを使ってグラフを描画しています
http://www.microsoft.com/ja-jp/download/details.aspx?id=14422
グラフには、データ数5000個のシリーズを5000個プロットしたいです
試しに、データ数100個で固定してプログラムを書いてみましたが、とても遅く実用できそうにありません
書いてみたコードは下記です。
System::Diagnostics::Stopwatch^ sw = gcnew System::Diagnostics::Stopwatch();//処理時間測定 sw->Start();//処理時間測定 int index = 0; for(i=0; i<series_count; i++) { //シリーズを作る this->chart1->Series->Add("i" + i.ToString()); this->chart1->Series["i" + i.ToString()]->ChartType = SeriesChartType::Line; this->chart1->Series["i" + i.ToString()]->Color = Color::Red; //プロットする for(j=0; j<100; j++) { // Chart描画 this->chart1->Series["i" + i.ToString()]->Points->AddY(data_array[index + j]); } index += 100; } sw->Stop();//処理時間測定終了 Debug::WriteLine("処理時間 = " + sw->ElapsedMilliseconds.ToString() + "msec");
変数series_countがシリーズ数です。
この値を変えて、Stopwatchクラスから出力される処理時間を見てみたところ
series_count = 100のとき、65msec
series_count = 500のとき、878msec
series_count = 1000のとき、2973msec
という結果になり、シリーズ数が増えるとかなり遅くなることが分かりました。
さらにseries_count = 1000ぐらいになってくると、グラフ描画する時点でPCの画面が固まり30秒程度操作不可になりだします。これは原因は分かりませんが、メモリ不足でスワップが発生している気がしています。
Chartコントロールの描画を速くする解決策をどなかた教えて頂けないでしょうか?
回答
-
Microsoft Chart Controls(VS2010では標準で使える)は、使うべき理由が特に生じなかったため調べていませんでしたが
適切にラップしてやっておけば、開発(デバッグとかのとき)にそれなりに役立つかも
また、コード上の外側から見た構造とか、デザインとかのアイデアも参考になるかも
とふと思ったので、いじってみました。
>メモリ不足でスワップが発生している気がしています。
メモリ使用量によってはそういう事もあり得るとは思いますが
普通はそうでない要因で少なくとも「30秒程度操作不可」といったことは起きるかと思われます。(メモリ使用量を見ての推測)
『OutOfMemoryException』についてはそれぞれの型や下準備が不明瞭なのでコメントは控えますが
佐祐理さんの「C++言語としてのオブジェクト参照」はToStringのみじゃなくて
this->chart1->Series["i" + i.ToString()]
にかかってるものだと思いました。(連想配列的なものを実装した事がある身としてはむしろこっちの方が主題に思います。)
仮にこの点に注意しつつarray<array<double>^>^
をつかってDataBindYを使った場合
//仮に↓とし private: typedef array<array<double>^> ARRAY; ARRAY^ data_array; SERIES_NUMがseriesの数で Y_NUMが現状100のところの数字として ///どこかの初期化関数とかでdata_arrayに数値を適当入力しておく data_array = gcnew ARRAY(SERIES_NUM); for ( int i = 0; i<SERIES_NUM; ++i ){ array<double>^ d = data_array[i] = gcnew array<double>(Y_NUM); pin_ptr<Double> p = &d[0]; //pin for ( int j = 0; j<Y_NUM; ++j ) p[j] = i*Y_NUM+j; p = nullptr; //unpin } /* Series^に保持することで余計なアクセスを減らしつつ data_array[i](つまり型はarray<double>^に等しい)をDataBindY */ for(int i=0; i<SERIES_NUM; ++i){ Series^ const se = chart1->Series->Add("i"+i); se->ChartType = SeriesChartType::Line; se->Color = Color::Red; se->Points->DataBindY( data_array[i] ); }
こんな感じの事ではないですかね?
こういったことで確かに多少速くなると思いますが
少なくともいずれの方法を試みても
『計測が終わった後の最初の描画(内部計算&Bitmapに反映の一連のながれ?)』
がけた外れに遅いという事実は変えられませんでした。
バインドよりもAddY連続の方が「最初の生成は」速い傾向があり、その代わりバインドは描画が速くなりました
それはただし、「Y要素数(?)」がかなり多い時の話で
Series数の方向へやたらと多い場合は
少なくともDataBindYをSeries毎にやることではあまり変わりがありませんでした。
「Y要素数」を100個から2個に(50分の1!)減らしても、依然として何十秒もかかります。
(Series一個ずつに対してなんらかの遅すぎる処理があるんではないかと)
複数のSeriesをひとくくりかのように扱えるバインディングの方法がもしかしてあれば別、かもしれませんが
普通は
世の中にあるこういう系統のグラフって、ほとんど系列10もないぐらいじゃないでしょうか?
なので100とか500とか1000とか5000とかって
そもそも想定されてない
よってけた外れに遅くなっても致し方ない類のこと
という予感は、しなくもないです。
表示上の問題もありますし
普通はそんなにシリーズ数必要ないだろう、ということで
同時最大表示系列数
は削る方向で検討するのが一番手っ取り早いと思います。
もしどうしても削らないという場合は、やっぱりMicrosoft Chart Controlsを使わず
ネイティブのお仕事(自分で座標制御込みでガリガリ書く)かもしれませんw
仮に1000*100=10万ポリゴンと見立てて、たとえばDirectX使えば、最近の並みのGPUなら普通1秒も絶対かからないようなものだと思います。
ただ、それやったとしても結局「モニタの限界」があることにかわりはないので
一番の本筋はやはり「同時最大表示系列数を減らす」かと思います。
- 編集済み mr.setup 2012年7月2日 12:54
- 回答としてマーク BB-X LARISSA 2012年7月5日 5:00
-
環境が全く異なるため比較にならないかもしれませんが、ずいぶんと結果が異なるようです。
こちらの環境はCore i7 8GB Win8 64bit VS2012 .NET 4.5 C#で32bitでコンパイルしています。それでもseries_count=1000、各seriesに100プロットで、データの設定に0.287秒、表示完了までに2~3秒です。32bitでコンパイルしているので使用可能なメモリ量に差はなく、質問者さんの環境でOutOfMemoryが発生するとしたら別の原因でしょう。(もしくはchartコントロールのバージョンの違い?)
私の書いたC#コードはこんな感じ。
var stopWatch = Stopwatch.StartNew(); var random = new Random(); for (var i = 0; i < 1000; i++) { var series = new Series("i" + i) { ChartType = SeriesChartType.Line, Color = Color.Red }; series.Points.DataBindY(Enumerable.Range(0, 100).Select(_ => random.NextDouble() * 100).ToArray()); chart1.Series.Add(series); } stopWatch.Stop(); Debug.WriteLine(stopWatch.ElapsedMilliseconds / 1000.0);
- 回答としてマーク BB-X LARISSA 2012年7月5日 5:00
-
折角なのでこちらの環境と結果もw
---------------------
※その前に、私自身が.NETにそれほど詳しくなくて結局2度調べることになったのでw
少なくとも佐祐理さんのコードについて
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;あたりはほしい、ということですね?(他にも必要物資あるかもしれないけど)
※これ書いとけばまたあとで別環境で実験するときもそこそこサクッといけるかな
---------------------
デバッグ起動だと表示完了までにさらに10秒ぐらい余分にかかりましたので、とりあえず
Text = "" + stopWatch ~
にかえさせていただいて通常起動だと
Athlon 64 X2 1.87GB/ WinXP 32bit / VS2010 C# EE .NET3.5で
(今となっては最近のPCと比べるとひよっこぐらいの性能になってしまったかもしれませんがw その中でどうにかやりくりしようとすると、結構何とかなってしまうもので、かわいいもんですw 7や8じゃないと使えないものが色々あるのでそれはそのうち欲しいですが、このXPには愛着があります。)
設定に0.836秒
表示完了までに33秒
※これは少なくともこちらの環境では、私が提示したC++/CLIのコードとほぼ同じ数値となりました。
な~るほど、この中に挙げられている環境要因の中でここまで大きく差がつく点があるのだとしたら・・・さすがにコンパイルは32bitということですし、プロセッサだけでは、そこまではたぶんなぁ・・・
chartコントロールは.NET 4.5で改善されたとかかな?
しっかし、こうしてみるとやっぱC#簡潔で便利ですねw
(インテリセンス効いてる状況でvarが使えて、しかも対.NETなのに当たり前にラムダ式が使える)
やっぱりマネージコードだけに集中出来て、それをバリバリに書く部分は出来るだけC#でやった方が良さそうだな、と。
ただ、XP公式サポートの打ち切りはそれほど遠いわけではないはずのため
これはもしかすると、古い環境を除外できるのであれば1000とかでも許容できる、ということを示唆しているのかもしれませんね。
- 編集済み mr.setup 2012年7月3日 5:03
- 回答としてマーク BB-X LARISSA 2012年7月5日 5:00
-
複数のSeriesをひとくくりかのように扱えるバインディングの方法がもしかしてあれば別、かもしれませんが
DataTableとか使えばできますかね。手元の環境ではそれほど違いは感じられませんでしたが。
# VS2010以降で実装されたautoを使ってます。
auto data = gcnew DataTable; Random random; for(auto i = 0; i < 1000; i++){ auto name = String::Concat(L"i", i); auto series = gcnew Series(name); series->ChartType = SeriesChartType::Line; series->Color = Color::Red; series->YValueMembers = name; chart1->Series->Add(series); data->Columns->Add(name, Double::typeid); } for(auto j = 0; j < 100; j++){ auto row = data->NewRow(); for(auto i = 0; i < 1000; i++){ auto name = String::Concat(L"i", i); row[name] = random.NextDouble() * 100; } data->Rows->Add(row); } chart1->DataSource = data;
あとは SeriesChartType::Line の代わりに SeriesChartType::FastLine とか。- 回答としてマーク BB-X LARISSA 2012年7月5日 5:01
すべての返信
-
ここに書かれているだけで見ても、series_count = 1000のとき
this->chart1->Series["i" + i.ToString()] が1000 * ( 3 + 100 ) = 103,000回実行されています。グラフとは無関係なC++言語としてのオブジェクト参照、これだけでも遅くなりそうに感じませんか?正攻法としては系列へのデータのバインドだと思います。
ところでSeriesが1000あるのはどんなグラフなんだろう…コンピューターのモニターは1000px程度しかありませんよ?
- 編集済み 佐祐理 2012年7月2日 6:50
-
回答ありがとうございます
ToString()の参照は確かに無意味だったので、なおしました。
教えて頂いたURLのPoints.DataBindを使って実装してみました。
System::Diagnostics::Stopwatch^ sw = gcnew System::Diagnostics::Stopwatch();//処理時間測定 sw->Start();//処理時間測定 int index = 0; for(y=0; y<series_count; y++) { y_string = y.ToString(); r_name = "i" + y_string; this->chart1->Series->Add(r_name); this->chart1->Series[r_name]->ChartType = SeriesChartType::Line; this->chart1->Series[r_name]->Color = Color::Red; //プロットする for(x=0; x<100; x++) { data[index] = data_array[x_index + x]; index++; } this->chart1->Series[r_name]->Points->DataBindY(data); x_index += 100; } Debug::WriteLine("処理時間 = " + sw->ElapsedMilliseconds.ToString() + "msec");
この処理だと、か遅くなってしまいました。
series_count = 100のとき、13037msec
series_count = 500のとき、OutOfMemoryException発生
series_count = 1000のとき、OutOfMemoryException発生他のバインドのメソッドも試してみたいのですが、色々調べて試していますが、使い方がいまいち分かっていません。
> コンピューターのモニターは1000px程度しかありませんよ?
言われてみるとそうだなって気付きました、、、
できるかぎり速度はあげたいことはかわりませんが、これはこれで重要になので検討します。
-
Microsoft Chart Controls(VS2010では標準で使える)は、使うべき理由が特に生じなかったため調べていませんでしたが
適切にラップしてやっておけば、開発(デバッグとかのとき)にそれなりに役立つかも
また、コード上の外側から見た構造とか、デザインとかのアイデアも参考になるかも
とふと思ったので、いじってみました。
>メモリ不足でスワップが発生している気がしています。
メモリ使用量によってはそういう事もあり得るとは思いますが
普通はそうでない要因で少なくとも「30秒程度操作不可」といったことは起きるかと思われます。(メモリ使用量を見ての推測)
『OutOfMemoryException』についてはそれぞれの型や下準備が不明瞭なのでコメントは控えますが
佐祐理さんの「C++言語としてのオブジェクト参照」はToStringのみじゃなくて
this->chart1->Series["i" + i.ToString()]
にかかってるものだと思いました。(連想配列的なものを実装した事がある身としてはむしろこっちの方が主題に思います。)
仮にこの点に注意しつつarray<array<double>^>^
をつかってDataBindYを使った場合
//仮に↓とし private: typedef array<array<double>^> ARRAY; ARRAY^ data_array; SERIES_NUMがseriesの数で Y_NUMが現状100のところの数字として ///どこかの初期化関数とかでdata_arrayに数値を適当入力しておく data_array = gcnew ARRAY(SERIES_NUM); for ( int i = 0; i<SERIES_NUM; ++i ){ array<double>^ d = data_array[i] = gcnew array<double>(Y_NUM); pin_ptr<Double> p = &d[0]; //pin for ( int j = 0; j<Y_NUM; ++j ) p[j] = i*Y_NUM+j; p = nullptr; //unpin } /* Series^に保持することで余計なアクセスを減らしつつ data_array[i](つまり型はarray<double>^に等しい)をDataBindY */ for(int i=0; i<SERIES_NUM; ++i){ Series^ const se = chart1->Series->Add("i"+i); se->ChartType = SeriesChartType::Line; se->Color = Color::Red; se->Points->DataBindY( data_array[i] ); }
こんな感じの事ではないですかね?
こういったことで確かに多少速くなると思いますが
少なくともいずれの方法を試みても
『計測が終わった後の最初の描画(内部計算&Bitmapに反映の一連のながれ?)』
がけた外れに遅いという事実は変えられませんでした。
バインドよりもAddY連続の方が「最初の生成は」速い傾向があり、その代わりバインドは描画が速くなりました
それはただし、「Y要素数(?)」がかなり多い時の話で
Series数の方向へやたらと多い場合は
少なくともDataBindYをSeries毎にやることではあまり変わりがありませんでした。
「Y要素数」を100個から2個に(50分の1!)減らしても、依然として何十秒もかかります。
(Series一個ずつに対してなんらかの遅すぎる処理があるんではないかと)
複数のSeriesをひとくくりかのように扱えるバインディングの方法がもしかしてあれば別、かもしれませんが
普通は
世の中にあるこういう系統のグラフって、ほとんど系列10もないぐらいじゃないでしょうか?
なので100とか500とか1000とか5000とかって
そもそも想定されてない
よってけた外れに遅くなっても致し方ない類のこと
という予感は、しなくもないです。
表示上の問題もありますし
普通はそんなにシリーズ数必要ないだろう、ということで
同時最大表示系列数
は削る方向で検討するのが一番手っ取り早いと思います。
もしどうしても削らないという場合は、やっぱりMicrosoft Chart Controlsを使わず
ネイティブのお仕事(自分で座標制御込みでガリガリ書く)かもしれませんw
仮に1000*100=10万ポリゴンと見立てて、たとえばDirectX使えば、最近の並みのGPUなら普通1秒も絶対かからないようなものだと思います。
ただ、それやったとしても結局「モニタの限界」があることにかわりはないので
一番の本筋はやはり「同時最大表示系列数を減らす」かと思います。
- 編集済み mr.setup 2012年7月2日 12:54
- 回答としてマーク BB-X LARISSA 2012年7月5日 5:00
-
環境が全く異なるため比較にならないかもしれませんが、ずいぶんと結果が異なるようです。
こちらの環境はCore i7 8GB Win8 64bit VS2012 .NET 4.5 C#で32bitでコンパイルしています。それでもseries_count=1000、各seriesに100プロットで、データの設定に0.287秒、表示完了までに2~3秒です。32bitでコンパイルしているので使用可能なメモリ量に差はなく、質問者さんの環境でOutOfMemoryが発生するとしたら別の原因でしょう。(もしくはchartコントロールのバージョンの違い?)
私の書いたC#コードはこんな感じ。
var stopWatch = Stopwatch.StartNew(); var random = new Random(); for (var i = 0; i < 1000; i++) { var series = new Series("i" + i) { ChartType = SeriesChartType.Line, Color = Color.Red }; series.Points.DataBindY(Enumerable.Range(0, 100).Select(_ => random.NextDouble() * 100).ToArray()); chart1.Series.Add(series); } stopWatch.Stop(); Debug.WriteLine(stopWatch.ElapsedMilliseconds / 1000.0);
- 回答としてマーク BB-X LARISSA 2012年7月5日 5:00
-
折角なのでこちらの環境と結果もw
---------------------
※その前に、私自身が.NETにそれほど詳しくなくて結局2度調べることになったのでw
少なくとも佐祐理さんのコードについて
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;あたりはほしい、ということですね?(他にも必要物資あるかもしれないけど)
※これ書いとけばまたあとで別環境で実験するときもそこそこサクッといけるかな
---------------------
デバッグ起動だと表示完了までにさらに10秒ぐらい余分にかかりましたので、とりあえず
Text = "" + stopWatch ~
にかえさせていただいて通常起動だと
Athlon 64 X2 1.87GB/ WinXP 32bit / VS2010 C# EE .NET3.5で
(今となっては最近のPCと比べるとひよっこぐらいの性能になってしまったかもしれませんがw その中でどうにかやりくりしようとすると、結構何とかなってしまうもので、かわいいもんですw 7や8じゃないと使えないものが色々あるのでそれはそのうち欲しいですが、このXPには愛着があります。)
設定に0.836秒
表示完了までに33秒
※これは少なくともこちらの環境では、私が提示したC++/CLIのコードとほぼ同じ数値となりました。
な~るほど、この中に挙げられている環境要因の中でここまで大きく差がつく点があるのだとしたら・・・さすがにコンパイルは32bitということですし、プロセッサだけでは、そこまではたぶんなぁ・・・
chartコントロールは.NET 4.5で改善されたとかかな?
しっかし、こうしてみるとやっぱC#簡潔で便利ですねw
(インテリセンス効いてる状況でvarが使えて、しかも対.NETなのに当たり前にラムダ式が使える)
やっぱりマネージコードだけに集中出来て、それをバリバリに書く部分は出来るだけC#でやった方が良さそうだな、と。
ただ、XP公式サポートの打ち切りはそれほど遠いわけではないはずのため
これはもしかすると、古い環境を除外できるのであれば1000とかでも許容できる、ということを示唆しているのかもしれませんね。
- 編集済み mr.setup 2012年7月3日 5:03
- 回答としてマーク BB-X LARISSA 2012年7月5日 5:00
-
複数のSeriesをひとくくりかのように扱えるバインディングの方法がもしかしてあれば別、かもしれませんが
DataTableとか使えばできますかね。手元の環境ではそれほど違いは感じられませんでしたが。
# VS2010以降で実装されたautoを使ってます。
auto data = gcnew DataTable; Random random; for(auto i = 0; i < 1000; i++){ auto name = String::Concat(L"i", i); auto series = gcnew Series(name); series->ChartType = SeriesChartType::Line; series->Color = Color::Red; series->YValueMembers = name; chart1->Series->Add(series); data->Columns->Add(name, Double::typeid); } for(auto j = 0; j < 100; j++){ auto row = data->NewRow(); for(auto i = 0; i < 1000; i++){ auto name = String::Concat(L"i", i); row[name] = random.NextDouble() * 100; } data->Rows->Add(row); } chart1->DataSource = data;
あとは SeriesChartType::Line の代わりに SeriesChartType::FastLine とか。- 回答としてマーク BB-X LARISSA 2012年7月5日 5:01
-
なるほど、DataTableだとこんな感じですか。
計測してみると・・・
44秒でした(ぬぅ)
あー、やっぱりいかなる手を使ってもこちらの環境だと1000もあると遅くなる
という可能性が高くなりました。
ただ、それ以外全部そのままで " SeriesChartType::FastLine "に変えてみると
9~10秒程度になりましたw(はやっ ・・・てかFastLineじゃないと遅すぎるのかw)
それでもフツーに「円滑にアプリを使いたい」と言う風に考えると十分遅いんですよね
でもやっぱり「古い環境を除外できる」なら、この辺ぐらいまでならありかもっていうきはしますね。
ただ、目標は5000シリーズ 5000個プロット・・・なんですよね?
数値をどちらも5000,5000に、さらに書き換えてのメモリ使用量を監視しての感想なのですが
FormのLoadイベントにコードを設置したところダブルクリックして30秒ぐらいたってもまずフォーム自体が表示されてこない(この間CPUは2コアの片方がぶん回りっぱなし)
↓
ついにメモリ使用量300MBの仮想メモリ使用量400MBを超える
↓
それでもまったくフォームが見える気配がない
となったので、恐ろしくなってその時点で終了させました。(w)
ちなみにお二方の環境でのメモリ使用量はどんなもんなのでしょうか?
こちらは1000, 100だと50MBぐらいですね。(アプリケーションの内容にもよりますが、大抵はこれでも「これだけの理由のために割くにしては」多くないかな?)
これはアプリケーション自体のメモリ使用量も込みなので、仮に実際には40MBぐらいだとして、単純計算出来ると仮定しても
40MB*5*50==9GB超!
となり、32bitでは絶対無理で、64bitでも一般的には(?)十分無理な気がします。
ちなみに、少し大きめに見積もって仮に一つの要素に8bytes使うとしても、全部自力でグラフを書くのならば
5000*5000*8bytes == 190MB程度
となり、Bitmapは画面出力用の最小限の大きさでいいので、普通にやってればあと数MBぐらいあるだけ、合わせて200MB程度で、これは状況によってなんとか許容範囲な気がします。
- 編集済み mr.setup 2012年7月3日 9:48
-
回答ありがとうございます
.NETのバージョンで、速度改善がなされているのですね
ちなみに私もFastLineにかえたら速度があがりました。
>ただ、目標は5000シリーズ 5000個プロット・・・なんですよね?
シリーズ数は制限できるようになりました
>それでもフツーに「円滑にアプリを使いたい」と言う風に考えると十分遅いんですよね
C#のテストプロで、FastLineで250シリーズ100プロットの場合に表示に丁度1秒なので、ここらあたりが限界なのかと思います。
ちなみにメモリ使用量ですが、
佐祐理さんの2012年7月2日 20:44のテストプロ(1000シリーズ,100プロット)を利用して、表示した場合に、メモリ空き容量が、1633000→1583000となったので、使用量は50KBでした。
自分の作ったC++/CLIのテストプロだとMB単位だったような気がしたんですが・・・何かそれはまた別の原因のようです
-
どうもどうも
シリーズ数さえ制限可能なら、各種問題は簡単に解決可能ですね。
ただ、1点ちょっと上記『自分のメモリ予想』だけ訂正しておきたいので、そのついでに
1633000→1583000
これって何で測りましたか?(この数値は、単位KBじゃないですかね? ※ちなみに私はAPIうったりするのが面倒なので、簡易チェックはタスクマネージャとかで行っていますが)
といいますのも、流石に.NETフレームワークのアプリ起動&Bitmap内部で使ってると考えると
MB単位はいってないとおかしいと思います。1000*100の要素があれば、1要素あたり1バイトしか使ってなくても100KB近く行ってしまいますし
単位がKBであれば、50MBとなり、こちらの環境とほぼ同じという事になります。
で、自分のメモリ予想の訂正、についてですが
0, 0
1000, 100
1000, 200
で試したところ、だいたい1000*100一つに対して27~30MBぐらい上昇する事が分かりました。
なので、そのまま直線的に増えた場合は、9GBはいかないですね。せいぜい7GB超ってとこのはずです。(どっちみち十分多いですがw)
※他の方が今後使う場合も、おそらくどの道シリーズ数低めに保つのが王道だと思うので、そう言う点では無用な心配でしょう。
- 編集済み mr.setup 2012年7月5日 5:42