アプリ版:「スタンプのみでお礼する」機能のリリースについて

Microsoft Visual Studio Community 2022 (64 ビット) - Current Version 17.5.5
のC#で大量のCSVファイルのデータを読み込んでそれを
WindowsフォームアプリではtextBoxにAppendTextで追加する方法と
コンソールアプリではConsole.WriteLineで追加する方法では、
表示速度がWindowsフォームアプリの方が圧倒的に遅いのですが
これは何が原因なのでしょうか?
またWindowsフォームアプリでこの処理を早くするにはどうすれば良いのでしょうか?

【コンソールアプリ】
public class Program
{
static void Main(string[] args)
{
TextFieldParser parser = new TextFieldParser("USDJPY.csv", System.Text.Encoding.GetEncoding("Shift_JIS"));
parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;
parser.SetDelimiters(",");
parser.ReadLine();
int cnt = 0;
DateTime dt_start = DateTime.Parse("2022-12-29 00:00:00");
DateTime dt_end = DateTime.Parse("2022-12-30 23:59:00");
while (parser.EndOfData == false)
{
string[] column = parser.ReadFields();

DateTime dt = DateTime.Parse(
column[0].Substring(0, 4) + "-" +
String.Format("{0:D2}", column[0].Substring(5, 2)) + "-" +
String.Format("{0:D2}", column[0].Substring(8, 2)) + " " +
String.Format("{0:D2}", column[1].Substring(0, 2)) + ":" +
String.Format("{0:D2}", column[1].Substring(3, 2)) + ":00");
Console.WriteLine(column[0]);
if (dt < dt_start) continue;
if (dt > dt_end) continue;
Console.WriteLine("■"+column[0]);
}
}
}

【Windowsフォームアプリ】
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void readCSV()
{
TextFieldParser parser = new TextFieldParser("USDJPY.csv", System.Text.Encoding.GetEncoding("Shift_JIS"));
parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;
parser.SetDelimiters(",");
parser.ReadLine();
int cnt = 0;
DateTime dt_start = DateTime.Parse("2022-12-29 00:00:00");
DateTime dt_end = DateTime.Parse("2022-12-30 23:59:00");
while (parser.EndOfData == false)
{
string[] column = parser.ReadFields();

DateTime dt = DateTime.Parse(
column[0].Substring(0, 4) + "-" +
String.Format("{0:D2}", column[0].Substring(5, 2)) + "-" +
String.Format("{0:D2}", column[0].Substring(8, 2)) + " " +
String.Format("{0:D2}", column[1].Substring(0, 2)) + ":" +
String.Format("{0:D2}", column[1].Substring(3, 2)) + ":00");
textBox1.AppendText(column[0]+Environment.NewLine);
if (dt < dt_start) continue;
if (dt > dt_end) continue;
textBox1.AppendText("■" + column[0] + Environment.NewLine);
}

}

private void button1_Click(object sender, EventArgs e)
{
readCSV();
}
}

質問者からの補足コメント

  • 添付1

    「大量のデータを読み込んで表示する速度を改」の補足画像1
    No.1の回答に寄せられた補足コメントです。 補足日時:2023/05/07 15:46
  • うーん・・・

    【Windowsフォームアプリ】
    using Microsoft.VisualBasic.FileIO;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using static System.Net.Mime.MediaTypeNames;
    using static System.Windows.Forms.VisualStyles.VisualStyleElement;

      補足日時:2023/05/07 15:49
  • うーん・・・

    namespace CSV
    {
    public partial class Form1 : Form
    {
    public Form1()
    {
    InitializeComponent();
    }

    private void readCSV()
    {
    TextFieldParser parser = new TextFieldParser("USDJPY.csv", System.Text.Encoding.GetEncoding("Shift_JIS"));
    parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;

      補足日時:2023/05/07 15:51
  • parser.SetDelimiters(",");
    parser.ReadLine();
    DateTime dt_start = DateTime.Parse("2022-12-29 00:00:00");
    DateTime dt_end = DateTime.Parse("2022-12-30 23:59:00");
    while (parser.EndOfData == false)
    {
    string[] column = parser.ReadFields();

      補足日時:2023/05/07 15:51
  • DateTime dt = DateTime.Parse(
    column[0].Substring(0, 4) + "-" +
    String.Format("{0:D2}", column[0].Substring(5, 2)) + "-" +
    String.Format("{0:D2}", column[0].Substring(8, 2)) + " " +
    String.Format("{0:D2}", column[1].Substring(0, 2)) + ":" +

      補足日時:2023/05/07 15:52
  • String.Format("{0:D2}", column[1].Substring(3, 2)) + ":00");
    textBox1.AppendText(column[0] + Environment.NewLine);
    if (dt < dt_start) continue;
    if (dt > dt_end) continue;

      補足日時:2023/05/07 15:52
  • textBox1.AppendText("■" + column[0] + Environment.NewLine);
    }

    }

    private void button1_Click(object sender, EventArgs e)
    {
    readCSV();
    }
    }
    }

      補足日時:2023/05/07 15:53
  • うーん・・・

    上のコードを繋げてください(使いづらい・・・)

      補足日時:2023/05/07 15:53
  • ちなみにUSDJPY.csvには
    2005.01.10,02:31,104.79,104.79,104.79,104.79,5
    2005.01.10,02:32,104.79,104.78,104.79,104.78,6
    2005.01.10,02:33,104.78,104.77,104.78,104.77,5
    のようなデータが67万行入っています(汗)

      補足日時:2023/05/07 15:56
  • 【コンソールアプリ】
    (TextFieldParserで呼び出し処理)
    long startcount;
    QueryPerformanceCounter(out startcount);

    while (parser.EndOfData == false)
    {
    Console.WriteLine(parser.ReadLine());
    }

    long endcount;
    QueryPerformanceCounter(out endcount);

    (結果表示)

    No.3の回答に寄せられた補足コメントです。 補足日時:2023/05/07 18:34

A 回答 (8件)

説明の仕方が悪かったですね。

申し訳ありません。

通常であれば、マルチスレッドのプログラミングをする場合、各スレッドから、同じ変数にアクセスする可能性がある場合は、排他制御を行うべきです。

つまり、rowは、readCSV()とbutton1_Click()から同時アクセスされる可能性があったので、Button1をdisableにしておけば、button1_Click()が呼び出されることもなく、修正が少なくて済むだろうと単純な考えでの提案でした。

なので訂正させてください。

buttonのdisablleはやめて、フォームload時に、セマフォを初期値0、最大値1で作成し、readCSV()を抜けるときに、セマフォをRelese()してください。

button1_Click()内で、enable/disableのチェック代わりに、WaitOne(0)で呼び出し、戻り値がfalseであれば、読み込みが終わってない状態なので、何もせず、trueであれば、処理を行うようにしてください。
https://learn.microsoft.com/ja-jp/dotnet/api/sys …

ボタンがdisableにはなりませんが、そこはmessageboxを表示するなり別途考えていただくということで、お願いします。

と、説明させていただきましたが。。。


結局、readCSV()がパフォーマンスに、影響がないということが、今回のテストで確認できたので、質問者さんが一番最初に作った670万回ループを、少し変更し、AppendText()部分だけど、今回の1000回に1回書き込むループに変更すれば、メモリのエラーもなくなり、パフォーマンスも劇的に向上するのではないかと思われます。

全体を読み込むのに、1分程度で、読み込めるのではないでしょうか?

つまり、threadも、semaphoreはもちろん、rowsや、button1のenabl/disableも必要なくなるわけです。
    • good
    • 0
この回答へのお礼

ご返信ありがとうございます。

たしかにreadCSV()ってそんなに時間がかかってなかったです。
自分はプロセスメモリを見て増えるのが止まったらbutton1を押してました。

1000回に1回書き込めば速度が速くなるのですが、ただやはり1行づつ見たいというのがありまして
コンソールアプリで表示させようか迷ってます・・・
ただそのVisual StudioのプロジェクトがメインがWindows フォームアプリなので
そこではコンソールフォームがどうやっても出てこないんです。。
この問題についはまた新たに別に質問を立てたいと思います。

この度はありがとうございました。

お礼日時:2023/05/09 10:12

追加で・・・



cnt++は、cnt %100 のif分の前に移動。

if文内で、cnt=0を追加

EODが検出され、whileを抜けた後に、cnt >0 ならば、残りのtextを追加でAppendTextしてください
    • good
    • 0
この回答へのお礼

ご回答ありがとうございます。

なるほど、たしかに自分のプログラムだと残りの1000行程度が追加されてない可能性がありました。

お礼日時:2023/05/09 10:11

アップロードありがとうございます



ソースコード拝見させていただきました。これで差分がほとんどないということは、AppendText()がほとんどのネックですね

今は、readCSV()のスレッドは、すぐに終わってるようなので、問題無いですが、rowsに対するの排他制御がないので、readCSV()が終わる前にボタンが押されると、デッドロックを起こしたり、全ての行を読み込む前に、foreachを抜ける可能性があると思うので、button1がクリックされる前に、ブロックする必要はあると思います(簡単なのは、bottun1は起動時は、disableで、readCSV()が終わる前に、bottun1をenableにするとか)


かなり気になるのが、質問者さんに実行していただいた1万行のテストパフォーマンスから単純計算すると、2.4時間で読み込みが完了するはずですが、実測で半日かかるということは、TextBoxサイズが肥大化していくと、極端にパフォーマンスが悪なる現象が起きてるのではないかと、推測できます


それを考えると、TextBoxに全てのデータをロードする、その設計自体を根本的に見直す必要があるのではと思います

と思って、調べてみるとありました。
https://www.ipentec.com/document/csharp-textbox- …

このサンプルをヒントにすると、foreach内のAppendTextをやめて、そこでは、単一のstringを構築する方法に変更にして(改行コードも追加し)ループを抜けたあとに、構築した単一のstringをAppendTextで追加するとどうなのか?それで、かなりのパフォーマンス改善が得られるのでは?と思いますが、どうでしょうか?
    • good
    • 0
この回答へのお礼

ご返信ありがとうございます。
こちらの都合でまた返信が遅れるかもしれませんがよろしくお願いいたします。

すみません、半日かかると書きましたが
だいぶ前に実行して試した時の記憶だったので、そんなにかかってなかったかもしれません(汗)
とにかく凄い時間がかかったというのはたしかです。

なるほど、AppendTextのネックですか。
そこでcntをインクリメントして
一定の回数ごとに(その時の)行の内容を情報をAppendTextに渡す方法で670行でのデータで試してみたのですが、

cnt%1000 == 0 処理時間:処理時間:8.20 秒

と劇的に早くなりました。
つまりこれは単純に計算して処理時間が1000分の1になったということですよね?
(ちなみに全ての行を表示させようとするとメモリのエラーが出るようです・・・添付画像を参照してください)
https://s.kota2.net/1683534766.png

ただ、そもそもTextBoxを利用しようとした目的が1行づつの進捗状況が見たかったからなので
やはりどうにかして1件1件が追加されたという情報を逐次、何かで知ることはできないでしょうか?

あとbottun1のdisable、enableの判定ですが、
readCSV()内で終わりにbutton1.Enabled = true;を追加すると

System.InvalidOperationException
HResult=0x80131509
Message=有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'button1' がアクセスされました。

というエラーがでます・・・
なのでisLoadedCSVという判定変数を用いてなんとかできないか考えてるのですが、
isLoadedCSV == trueならbutton1.Enabled = trueにする処理をどこで記述すれば良いのでしょうか?
https://ux.getuploader.com/dialogues_txt/downloa …

お礼日時:2023/05/08 17:43

今の状況を整理すると



67万行を一度に読み込むと、予測では14分程度待つ必要があるので、許容できないという前提で、それを解決できる方法を検討してると理解しています。

判明してることは、AppendText()をループで回すと、処理に時間がかかるので、1)ApendText()に変わる良い方法はあるのか?が一つ、2)ApendText()を使った場合どうやって解決すればいいか?がもうひとつ

自分からは、2)のApendText()を一度に呼ばない方法をメインで提案しています。


少し頭を冷やして、ApendText()を分割する方法をいくつか検討してみました

1)ボタン追加で、500件ごと読み込む方法
UIに、追加のボタンが許容できるのであれば「次の500件を読み込む」「前の500件に戻る」ボタンをUIに追加し、今のreadCSV()を、setupCSV()と、readCSV()に分割し、parserはフィールドに、また、while文をfor分に変更し、break条件を、EODか500までカウントアップする方法に変更します。ただし「前の500件に戻る」ボタンをつけることで、TextFieldParserでは遡っての、再読読み込みは実現が難しいので、ページ毎のTextBoxオブジェクトを1340個を持つ必要性と、どのページを表示しているか、enable/disableのコントロールや、既に読み込んだページと、これから読み込むページなどの管理が必要になります。

2)ファイルの一括読み込みと、ApendText()を分ける方法
プログラム起動時に、別スレッドでcsvファイルメモリ上に読み込み、parserを通した、stringオブジェクトを67万行分キープします(30Mbyte弱ほどになる予想)。ボタン1が押された場合には、AppendText()のみを実行し登録。予測ではAppendText()のみを67万回実行するのであれば、4分ぐらいで終わるのではと思っています。4分が許容できるかどうかですが。。。

3)スクロールバーのイベントから次の500ラインを読み込む方法
これが一番メモリへの影響などのインパクトが薄いですが、インプリメントや変更は多いと思います。ボタン1が押された後に、ファイルを分割して読み込む方法で、今の、readCSV()を分割して、setupCSV()とreadCSV()に分割します。readCSV()のwhile文をfor文に変えて、breakする条件を、EODか、500までのカウントアップに変更し条件に一致すれば終了します。
それと共に、TextBoxのVScrollbarのScrollイベントで、SB_BOTTOMを検出したら、次の500ラインを読み込む。としていけば、ストレスがなく、読み込めるのではないかと思ってます。EODが検出されれば、readCSV()は、実行しなくても良いので、より、高速になると思います。
これが参考になるかとは思います。
https://www.ipentec.com/document/csharp-detect-s …


どの方法が適しているかは、なんとも言えませんが、これ以外にも、まだ解決方法があるとは思います。

もし、これらの方法が難しいと考えるのであれば、一旦、この質問は閉じていただいて、他の方の意見を伺うために、67万行のcsvファイルをTextBoxにApendText()で追加する場合のパフォーマンスの改善できる良い方法はないか?という質問をしていただくのも手かとは思います。


念の為、500行というのは質問者さんがテスト結果から、500行だと0.6秒程度の予測なので、その程度れあれば、ほぼブロックされている感覚がないという前提の数字です。
    • good
    • 0
この回答へのお礼

ご返信ありがとうございます。
67万行が14分程度なら全然許容範囲です(半日ぐらいかかってましたから・・・汗)
とりあえず、ご提案があった2の方法を1万件のデータで試してみのですが、
処理時間:12.34 秒 とそんなに変わらなかったです・・・
ちょっとやり方を勘違いしている可能性があるので見てもらえないでしょうか。
文字数制限があるのでアップローダーでダウンロードをお願いします。
https://ux.getuploader.com/dialogues_txt/downloa …

お礼日時:2023/05/08 11:16

測定ありがとうございます。




1万行で、ほぼ10秒の差ということは、一回の呼び出し差は1msでしかないです。

これは、APIが遅いというレベルではなく、ループ回数が多すぎるということになるので、一度に全てを読み込むのは、設計上好ましくないという判断になると思います。

対応策は、読み込みを分割するのが適切で、初期読み込みは、500行程度(今のCPUで0.6秒程度の計算)終了し、スクロールエンドに来れば、再度読み込む、といった処理にするか、スレッドを作って、残りのデータを読み込むなどの方法で、分割すべき内容かとは思います


※間違えて自分自身で「いいね」を押してしまいました。お気になさらずに。
    • good
    • 0
この回答へのお礼

ご返信ありがとうございます。
すみません、先ほどから「読み込みの分割」というのが
いまいちわからないのですが、
これってReadLineで
while (parser.EndOfData == false)
{
textBox_Output.AppendText(parser.ReadLine() + Environment.NewLine);
}
一行づつ読み込んでますよね?
例えばReadMultiLine()みたいな関数があったとして
一気に1万行を読み込んでいたのなら、その分割した方が良いというのはわかるのですが、
この場合ってこれ以上どうやって読み込みを分割すれば良いのでしょうか?
※おそらく自分が勘違いしてるので、このような質問になってしまうのですがすみません

お礼日時:2023/05/07 20:00

元々の質問内容は「何が原因でしょうか?」ということでしたので、自分の仮説では、メモリアロケーションが要因だと思うので、SingleLineにした場合は、メモリアロケーションが発生しないと思われるので、Consoleと同程度のパフォーマンスが得られるのでは?という考えで、それを証明するために、時間計測を提案させていただきました。



つまり、

1)Console.WiteLine()と、SingleLineTextの開始から終わりまでの時間を測定すると、ほぼ、同程度で終わるのでは?と仮説
2)Console.WriteLine()を使った時に、それぞれのLine分のstringをメモリアロケーションして、コピーし計測した場合と、MultiLineTextの場合とほぼ同じ時間がかかるのでは?という仮説

で、その二つのケース測定した結果、それぞれテストの時間の差分がほぼなければ、csvファイルを一度に読み込む処理をやめて、分割してcsvファイルを読み込むという処理に変更せざるを得ないのでは?と思ってます

1000行程度読み込んで、残りはバックグラウンドで読み込むなどの設計変更が必要ではないのか?とは思っています
この回答への補足あり
    • good
    • 1
この回答へのお礼

ご返信ありがとうございます。

ちょっとそのサイトの計測の仕方を利用して計測してみました。
さすがに76万行だと多いので1万行でそれぞれ計測した結果です。

Windowsフォームアプリ:処理時間:12.58 秒
コンソールアプリ:処理時間:2.79 秒

これからどのようなことが分かりますでしょうか?
また対処法はどうなりますか?
ちなみに文字数オーバーなので、また補足にコードの一部を載せたいのですが、
足りるかなぁ・・・
コードの一部です
【Windowsフォームアプリ】
TextFieldParser parser = new TextFieldParser("USDJPY.csv", System.Text.Encoding.GetEncoding("Shift_JIS"));
parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;
parser.SetDelimiters(",");
parser.ReadLine();

long startcount;
QueryPerformanceCounter(out startcount);

while (parser.EndOfData == false)
{
textBox_Output.AppendText(parser.ReadLine() + Environment.NewLine);
}

long endcount;
QueryPerformanceCounter(out endcount);

double sec = (double)(endcount - startcount) / (double)freq;
textBox_Output.Text += string.Format("処理時間:{0:f} 秒\r\n", sec);

お礼日時:2023/05/07 18:33

うわ



流石に貼られるとは、思ってなかったですw

こういうデバッグをする場合には、計測して、ボトルネックが何処か、特定する事が大事だと思いますので、まずは、処理時間を計測をした方が良いと思います。
良いサンプルがありました
https://www.ipentec.com/document/csharp-using-qu …
    • good
    • 0
この回答へのお礼

ご返信ありがとうございます。

すみません、そのご紹介いただいたサイトの内容もそうなのですが、
計測とのことですがどこを計測するのかが分からないというか
つまり

while (parser.EndOfData == false)
{
textBox1.AppendText(parser.ReadLine() + Environment.NewLine);
}

このループの部分が時間がかかってることは分かるのですが、
これ以上どこを分解して測れば良いかが分からないのです・・・
AppendTextの実装の部分で何が行われてるのか分かれば良いのですがどうなんでしょう。。

お礼日時:2023/05/07 17:18

コードが全て見えているわけではないので、参考までに



コンソールアプリは、テキスト用のバッファーをアロケーションしません。そのため、メモリによるサイズ圧迫はないでしょう

一方で、質問者さんが使っているTextBoxはマルチラインだと思いますので、その分stringバッファを取ると思います。つまり、サイズが大きくなればなるほど、メモリのアロケーションが繰り返され、メモリを圧迫してるのではないでしょうか?

TextBoxをシングルラインにしても、同じようにパフォーマンスが落ちるのか?また、コンソールアプリの時に、テキストバッファをアロケーションした場合に、同じようにパフォーマンスが落ちるのか?

一つの判断はそこでしょうか?

メモリの圧迫であれば、タスクマネージャーを見てるだけでもわかるかもしれません
この回答への補足あり
    • good
    • 0
この回答へのお礼

ご回答ありがとうございます。
メモリは32GBあるので大丈夫だとは思うのですがどうなんでしょうか?
TextBoxをシングルラインに戻して実行してみたのですが、
なんせ1行しか見れず高速でテキストが見えたり見えなかったりしてるので
どのくらいの速度でデータがTextBoxに入ってるかが分からないです・・・
もしよろしければよっしーラボさんの方で次コードで
Visual Studioを利用できれば試していただけないでしょうか?

お礼日時:2023/05/07 15:48

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!

このQ&Aを見た人はこんなQ&Aも見ています


このQ&Aを見た人がよく見るQ&A