VBの関数の処理速度について

タグの編集
投稿者 凡人  (高校生) 投稿日時 2011/8/8 22:35:13
こんにちは。
Visual Basic 2008でアプリを作っている、凡人です。

今回実装する機能の中に、「csvファイルからListViewにアイテムを追加する」という処理を実装する必要があります。
この処理はアプリ全体を通して複数箇所で行われ、また複数のcsvファイルで同様の処理が行われるので、上記の処理を実行するその関数を作ったのですが...
処理速度があまりにも違いすぎます。

ここではcsvファイル、及び読み込み先のListViewを
・csv1 -> ListView1
・csv2 -> ListView2
・csv3 -> ListView3
とします。

1.)  [csv1を読み込み、ListView1に追加]という処理をcsvの数だけ用意し、複数箇所に同じコードを貼り付ける。 -> 一番早い[実行時間:10000]
2.) [csv1を読み込み、ListView1に追加]という処理の関数をcsvの数だけ用意し、複数箇所から関数をcallする -> 普通(?)    [実行時間:53000]
3.) [渡されたcsvのパスを読み込み、渡されたオブジェクトに追加]という処理の関数を作り、複数箇所からはcallする -> 遅い  [実行時間:130000]

なお、コードは上から順に 複雑(ムダ?) -> シンプル(?)となっています。

このアプリの実行環境は、必ずしも早いとは限らないので、実行速度の面は心配です。
またこのアプリはコードを公開する予定があるかもしれないので、あまり変な書き方もしたくないです。

皆さんならどうしますか?
またこれくらいの実行速度の差は、普通なのでしょうか?
助言等ございましたら、よろしくお願いします。


-------------------------
開発環境
Windows 7 (x64)
Visual Studio 2008 
-------------------------
投稿者 凡人  (高校生) 投稿日時 2011/8/8 22:42:41
[サンプル]
1.)の場合は以下のコードです。
このコードを複数箇所に設置し、csvファイルごとに作ります。


   'CSV入力 
            Dim Reader As IO.StreamReader
            Dim csvstr As String = String.Empty

            Reader = New IO.StreamReader(FileName, System.Text.Encoding.GetEncoding("shift_jis"))

            csvstr = Reader.ReadLine()
            Do Until csvstr Is Nothing
                Dim str() As String
                str = Split(csvstr, ",")
                'ちなみにここではCTypeは訳あって使えません。 
                Dim Array As String() = {str(0), str(1)}
                ListView1.Items.Add(New ListViewItem(Array))

                csvstr = Reader.ReadLine()
            Loop

            Reader.Close()
投稿者 凡人  (高校生) 投稿日時 2011/8/8 22:46:31
[サンプル2]
2.)の場合は以下のコードです。
これをcsvファイルごとに作って、callします。


 Private Sub CSVRead_1()
        'CSV入力  
        Dim Reader As IO.StreamReader
        Dim csvstr As String = String.Empty

        Reader = New IO.StreamReader(FileName, System.Text.Encoding.GetEncoding("shift_jis"))

        csvstr = Reader.ReadLine()
        Do Until csvstr Is Nothing
            Dim str() As String
            str = Split(csvstr, ",")
            Dim Array As String() = {str(0), str(1)}
            ListView1.Items.Add(New ListViewItem(Array))

            csvstr = Reader.ReadLine()
        Loop

        Reader.Close()
    End Sub
'該当個数分作る 
'以下呼び出し部分 

        Call CSVRead_1()

投稿者 凡人  (高校生) 投稿日時 2011/8/8 22:54:19
[サンプル3]
3.)の場合は以下のコードです。
このコードを複数ヶ所からcallします。


 ''' <summary> 
    ''' CSVを入力します。 
    ''' </summary> 
    ''' <param name="FileName_str">ファイル名</param> 
    ''' <param name="Object_To">処理対象のコントロール</param> 

   Private Sub Input_CSV(ByVal FileName_str As StringByVal Object_To As System.Object
        'CSV入力 
        Try
            Dim Reader As IO.StreamReader
            Dim csvstr As String = String.Empty

            Reader = New IO.StreamReader(FileName_str, System.Text.Encoding.GetEncoding("shift_jis"))

            csvstr = Reader.ReadLine()
            Do Until csvstr Is Nothing
                Dim str() As String
                str = Split(csvstr, ",")
                Dim Array As String() = {str(0), str(1)}               
                Object_To.Items.Add(New ListViewItem(Array))

                csvstr = Reader.ReadLine()
            Loop

            Reader.Close()
    End Sub

'以下呼び出し部分 
        Call Input_CSV (FileName , ListView1)



「Object型を使っているから遅いのか...?」とか考えてしまいましたが、よくわからないです...
サンプルは以上です。
よろしくお願いします。

※例外処理の部分は抜かしてあります。
投稿者 shu  (社会人) 投稿日時 2011/8/8 23:12:15
ファイルサイズがどの位かわかりませんが、そんなに大きいファイルでなければ
ReadToEndで全部読み込んでメモリ内処理にしたほうが速いと思います。
全体を読むのが無理ならReadBlockで読むかBinaryReaderで固定byte数づつ読むとかも
効果的だと思います。

ListViewへのAddも1行毎に行うよりも一括で追加した方が速いと思います。

> Object_To.Items.Add(New ListViewItem(Array))
は遅延バインディングになるので速度低下を招きます。
投稿者 通りすがり  (社会人) 投稿日時 2011/8/8 23:27:42
ListViewならBeginUpdateとEndUpdateで挟むと速くなりますよ。

        ListView1.BeginUpdate()
        For i As Integer = 0 To 4000
            ListView1.Items.Add(New ListViewItem(New String() {"0""1"}))
        Next
        ListView1.EndUpdate()
投稿者 凡人  (高校生) 投稿日時 2011/8/11 17:20:25
shu様、通りすがり様、ご回答ありがとうございました。

>shu様
>ListViewへのAddも1行毎に行うよりも一括で追加した方が速いと思います。

「一度ListViewItemCollectionに溜め込み、DataGridViewのように、溜め込んだデータをソースとして読み込む」という方法を考えついたのですが、ListViewItemCollectionのリファレンスがあまり見つからず、MSDNを探してもよくわかりませんでした。
他にListViewにItemを一括登録する方法、もしくはListViewItemCollectionからの登録方法は、ありますか?

以下、自分なりに作ってみたコードです。


    ''' <summary> 
    ''' CSVを入力します。 
    ''' </summary> 
    ''' <param name="FileName_str">ファイル名</param> 
    ''' <param name="Object_To">処理対象のコントロール</param> 

    Private Sub Input_CSV(ByVal FileName_str As StringByVal Object_To As ListView)
        'CSV入力 
        
        Dim Reader As IO.StreamReader

        '読み込み処理 
        Reader = New IO.StreamReader(FileName_str, System.Text.Encoding.GetEncoding("shift_jis"))
        Dim ReadedTxt() As String = Split(Reader.ReadToEnd, vbCrLf )
        Reader.Close()

        Dim ListViewItemArray(UBound(ReadedTxt) - 1) As ListView.ListViewItemCollection

        For i = 0 To UBound(ReadedTxt) - 1
            Dim str() As String = Split(ReadedTxt(i), ",")
            Dim Into As String() = {str(0), str(1)}
            ListViewItemArray(i).Add(New ListViewItem(Into))
        Next


        Object_To.BeginUpdate()
        'ListView追加処理 
        'ListViewItemCollectionから追加したい... 

        Object_To.EndUpdate()

    End Sub


なお、一行一行で追加していく場合には以下のコードを作成しました。

    ''' <summary> 
    ''' CSVを入力します。 
    ''' </summary> 
    ''' <param name="FileName_str">ファイル名</param> 
    ''' <param name="Object_To">処理対象のコントロール</param> 

    Private Sub Input_CSV(ByVal FileName_str As StringByVal Object_To As ListView)
        'CSV入力 
        
        Dim Reader As IO.StreamReader
        Dim ListItemArray As New ArrayList

        '読み込み処理 
        Reader = New IO.StreamReader(FileName_str, System.Text.Encoding.GetEncoding("shift_jis"))
        Dim ReadedTxt() As String = Split(Reader.ReadToEnd, vbCrLf )
        Reader.Close()

        For i = 0 To UBound(ReadedTxt) - 1
            Dim str() As String = Split(ReadedTxt(i), ",")
            Dim Into As String() = {str(0), str(1)}
            ListItemArray.Add(Into)
        Next


        Object_To.BeginUpdate()

        'ListView追加処理 

        For Each Into As String() In ListItemArray
            Object_To.Items.Add(New ListViewItem(Into))
        Next

        Object_To.EndUpdate()

    End Sub


よろしくお願いします。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/11 18:32:35
一括で登録するには、.Items.AddRange を使います。
.Items.Add を繰り返すよりもかなり高速に処理できます。

> 一度ListViewItemCollectionに溜め込み、DataGridViewのように、溜め込んだデータをソースとして読み込む
いわゆる「データバインド」と呼ばれる機能を使いたいなら、DataGrid や DataGridView を
使った方が良いでしょう。ListView は DataSource を指定することができません。

もしも AddRange でもまだ遅いなら、ListView.VirtualMode を使うぐらいしか無いと思いますよ。
投稿者 凡人  (高校生) 投稿日時 2011/8/16 21:03:41
魔界の仮面弁士 様、ありがとうございました。

>一括で登録するには、.Items.AddRange を使います。
色々と試したのですが...


  Dim ListViewItemArray(UBound(ReadedTxt) - 1) As ListView.ListViewItemCollection
  'アイテムをコレクションに追加します。 
  Dim Into As String() = {str(0), str(1) }
  
  For i = 0 To UBound(ReadedTxt) - 1
      ListViewItemArray(i).Add(New ListViewItem(Into))

  '(省略) 

  'ListViewに追加します 
  Object_To.Items.AddRange(ListViewItemArray)  '※1 


※1で、「これらの引数で呼び出される、アクセス可能な 'AddRange' がないため、オーバーロードの解決に失敗しました...」となり、エラーが発生してしまいます。

[MSDN該当ページ]
http://msdn.microsoft.com/ja-jp/library/bfzz668s(v=VS.80).aspx


Items.AddRangeの使い方、もしくはItems.AddRangeのサンプルをお願いできますでしょうか?
よろしくお願いします。
投稿者 shu  (社会人) 投稿日時 2011/8/16 21:30:36
配列を使うならこんな感じ
        Dim Items(4) As ListViewItem

        Items(0) = New ListViewItem(New String() {"a", "b"})
        Items(1) = New ListViewItem(New String() {"c", "d"})
        Items(2) = New ListViewItem(New String() {"e", "f"})
        Items(3) = New ListViewItem(New String() {"g", "h"})
        Items(4) = New ListViewItem(New String() {"i", "j"})

        ListView1.Items.AddRange(Items)

リストを使うならこんな感じ
        Dim ListItems as new List(Of ListViewItem)

        ListItems.Add(New ListViewItem(New String() {"a", "b"}))
        ListItems.Add(New ListViewItem(New String() {"c", "d"}))
        ListItems.Add(New ListViewItem(New String() {"e", "f"}))
        ListItems.Add(New ListViewItem(New String() {"g", "h"}))
        ListItems.Add(New ListViewItem(New String() {"i", "j"}))

        ListView1.Items.AddRange(ListItems.ToArray())


投稿者 魔界の仮面弁士  (社会人) 投稿日時 2011/8/16 22:11:30
>一括で登録するには、.Items.AddRange を使います。
.Items.Add を BeginUpdate / EndUpdate 併用で呼び出した場合は、
.Items.AddRange と同等の時間で済みます。
(BeginUpdate / EndUpdate なしでは大きな差が付きますが)

これらの方法をとっても十分な速度向上が見込めない場合には、先にも書いたように
VirtualMode を利用するか、別のコントロールで代用するなどといった大幅な修正が必要になります。


> Dim ListViewItemArray(UBound(ReadedTxt) - 1) As ListView.ListViewItemCollection
> Object_To.Items.AddRange(ListViewItemArray)  '※1 

AddRange メソッドの引数定義は、
 Public Sub AddRange ( items As ListViewItemCollection )
もしくは
 Public Sub AddRange ( items() As ListViewItem )
の 2 種類です。

しかし作成されたコードでは、
 Public Sub AddRange ( items() As ListViewItemCollection )
を呼び出そうとしていることになってしまいますね。


> Items.AddRangeの使い方、もしくはItems.AddRangeのサンプルをお願いできますでしょうか?

Public Class Form1

    Private sampleData() As String = Enumerable.Range(0, 50000).Select(Function(i) i.ToString("000000")).ToArray()

    Private Sub Button1_Click() Handles Button1.Click
        ListView1.Clear()

        Dim sw = Stopwatch.StartNew()
        ListView1.BeginUpdate()

        If CheckBox1.Checked Then
            '一括登録 
            Dim items As New List(Of ListViewItem)()
            For Each item In sampleData
                items.Add(New ListViewItem(item))
            Next
            ListView1.Items.AddRange(items.ToArray())
        Else
            '繰り返し登録 
            For Each item In sampleData
                ListView1.Items.Add(item)
            Next
        End If

        ListView1.EndUpdate()
        sw.Stop()

        MessageBox.Show(sw.Elapsed.ToString(), "処理時間")
    End Sub

    Private Sub Button2_Click() Handles Button2.Click
        ListBox1.Items.Clear()

        Dim sw = Stopwatch.StartNew()
        ListBox1.BeginUpdate()

        If CheckBox1.Checked Then
            '一括登録 
            ListBox1.Items.AddRange(sampleData)
        Else
            '繰り返し登録 
            For Each item In sampleData
                ListBox1.Items.Add(item)
            Next
        End If

        ListBox1.EndUpdate()
        sw.Stop()

        MessageBox.Show(sw.Elapsed.ToString(), "処理時間")
    End Sub
End Class
投稿者 凡人  (高校生) 投稿日時 2011/8/16 22:45:09
shu様 魔界の仮面弁士様、ありがとうございました。
ソースコードいろいろと勉強になりました。
私は、Now.Ticksを使って時間を計算していましたが、Stopwatchというクラスもあったとは・・・
無事動作させることができました。

-----------------------------

自分のコードを公開するのが恥ずかしくて、このような掲示板を使うのは初めてでしたが...
おかげ様で、VBの理解をより深める事ができました。

皆様、本当にありがとうございました!