高速化 | Excel作業をVBAで効率化 https://vbabeginner.net いつものExcel作業はVBAを使えば数秒で終わるかもしれませんよ Sun, 10 Nov 2024 14:52:35 +0000 ja hourly 1 https://wordpress.org/?v=6.6.2 https://vbabeginner.net/wp-content/uploads/2019/02/favicon-150x150.png 高速化 | Excel作業をVBAで効率化 https://vbabeginner.net 32 32 VBAの高速化(事前領域確保で文字列連結を高速に行う) https://vbabeginner.net/high-speed-string-concat/ Sat, 17 Aug 2019 18:17:56 +0000 https://vbabeginner.net/?p=4436 &での文字列連結は遅い

文字列を連結する場合、通常は「a = “abc” & “cde”」のように&を使って行います。

&を使った文字列の連結処理は一般的に行われるものですが、連結結果は元のメモリ領域とは別の領域に対して行うため新しい領域確保が必要になり、その領域確保はVBAに限らずどのようなプログラミング言語でも時間が掛かる処理になります。

これが&での文字列連結に時間が掛かる理由です。連結の回数に比例してどんどん遅くなります。

Midステートメントでの書き換えは速い

&の文字列連結で時間が掛かるのは新たに領域の確保を行うことが原因のためで、言い方を変えれば、新しい領域の確保をせずに文字列の連結を行うことが出来れば、処理は速くなります。これも他のプログラミング言語でも同じです。

VBAで扱う文字列は通常は領域サイズを指定しませんが、事前に指定した領域を確保することは可能です。

ただ、&での文字列連結をすると新しい領域の確保が発生してしまうため、別の方法で文字列連結を行う必要があります。具体的には文字列連結ではなく、確保済みの領域をMidステートメントで書き換えることで実現します。

Midステートメントは既に確保済みの領域の一部の書き換えだけを行うため、領域の再確保が発生しない分、&よりも高速に処理されます。

以下のコードで、一般的な文字列連結の場合と、事前に領域を確保した場合でどれぐらいの違いがあるのかを紹介します。

一般的な文字列連結のコード

以下のコードは10万回のループで1文字ずつ文字列変数に連結しています。

Sub StringCat1()
    Dim s       As String
    Dim lSize
    Dim i
    
    lSize = 100000
    i = 1
    Do
        If (i > lSize / 2) Then
            Exit Do
        End If
        
        '// 文字列を連結
        s = s & "1"
        
        i = i + 1
    Loop
End Sub

事前に領域を確保して文字列連結するコード

事前に文字列のサイズを確保して10万回のループで1文字ずつ文字列変数を書き換えています。

書き換えはMidステートメントで対象位置を指定の文字に対して行います。ループ終了後に書き換えを行った部分だけを文字列変数に再設定しています。

MidステートメントはMid関数と同じ名前ですが用途が異なります。Mid関数は文字列の一部を返しますが、Midステートメントは文字列の一部を書き換えます。

Sub StringCat2()
    Dim s   As String
    Dim lSize
    Dim i
    
    lSize = 100000
    
    '// 指定サイズの領域を確保
    s = String(lSize, 0)
    
    i = 1
    Do
        If (i > lSize / 2) Then
            Exit Do
        End If
        
        '// 指定位置の文字列を書き換え
        Mid(s, i, 1) = "1"
        
        i = i + 1
    Loop
    
    '// 連結した部分のみを残す
    s = Mid(s, 1, i - 1)
End Sub

速度比較

上記の2つのコードを実行してどれぐらいの差があるのかそれぞれ10回ずつ実行して比較します。

Sub StringCatTest()
    Dim t1
    Dim t2
    Dim t3
    Dim i
    
    For i = 1 To 10
        t1 = Timer
        Call StringCat1     '// 通常時
        t2 = Timer
        Call StringCat2     '// 事前確保時
        t3 = Timer
        
        Debug.Print "通常時:" & CStr(t2 - t1)
        Debug.Print "事前確保時:" & CStr(t3 - t2)
    Next
End Sub

実行結果の平均は通常時は約0.16秒、事前確保時が0.006秒でした。単純比較では26倍程度の差があります。

さらに10万回ループを100万回ループで行うと、約80秒と約0.07秒の1000倍以上の差が出ました。回数に比例することがこのことからも分かります。

事前に領域を確保する方法は処理速度を要求される場合には有効な手段ですが、Midステートメントがあまり見慣れないコードのため、直観的に分かりにくいコードなのが欠点です。

]]>
VBAで印刷設定を高速に行う https://vbabeginner.net/set-print-settings-high-speed/ Tue, 13 Aug 2019 18:18:15 +0000 https://vbabeginner.net/?p=4410 印刷設定が遅い理由

VBAに限らず、Excelでの印刷設定関連の操作は印刷しようがしまいが関係なく、ことごとく遅いです。

遅い理由は、印刷設定を行うWorksheetオブジェクトなどのPageSetupオブジェクトを操作する度にプリンターと通信を行うためです。

しかし実際の印刷までの一連の作業では、事前に印刷範囲の確認をしたりヘッダーやフッターの設定だけをしたいような印刷設定を行うだけの場合の方が多く、設定だけしかしないのに、不必要なプリンターとの通信で処理が遅くなることは迷惑でしかありません。

実際の印刷はPageSetupオブジェクトとは別のPrintOutメソッドで行うため、本来であれば印刷の設定(PageSetup)ではプリンターと通信する必要がないことから、Excel自体の印刷周りの設計や実装に不備があるのかもしれません。とは言っても言語仕様がこうなってる以上、仕方ありません。

問題なのはPageSetupが遅いことなので、これを回避できれば問題は解決します。回避する方法は2通りあります。

印刷設定が遅くなることを回避する方法

PrintCommunicationプロパティにFalseを設定して回避する

一番簡単な方法はApplication.PrintCommunicationプロパティをFalseに設定することです。Falseを設定するとプリンターとの接続を切断します。Trueは接続します。

PageSetupオブジェクトの設定を行う直前にPrintCommunicationプロパティにFalseを設定し、PageSetupプロパティの設定が終わったらTrueを設定すればプリンターとの通信を行わないため、印刷設定を高速に行うことが可能です。

なお、PrintCommunicationプロパティはExcel2010より古いバージョンでは利用できません。

コードは以下のような感じになります。

Sub PrintCommunicationTest()
    '// プリンタとの接続を切断
    Application.PrintCommunication = False
    
    '// 印刷設定
    With ActiveSheet.PageSetup
        .CenterHeader = "タイトル"
        .RightHeader = "&D"
        .CenterFooter = "&P" & "/" & "&N"
    End With
    
    '// プリンタと接続する
    Application.PrintCommunication = True
End Sub

Excel4.0のExecuteExcel4Macroメソッドを使って回避する

Application.PrintCommunicationプロパティはExcel2010で追加された機能のため、それより古いExcelの場合では利用できません。

その場合は、Excel4.0マクロを使うための「ExecuteExcel4Macro」メソッドで、「Page」オブジェクトの「Setup」メソッドを使うと高速に印刷関連の処理を行うことができます。

構文は以下の通り、引数だらけです。

PAGE.SETUP(Header, Footer, LeftMargin, RightMargin, TopMargin, BottomMargin, PrintHeadings, PrintGridlines, CenterHorizontally, CenterVertically, Orientation, PaperSize, Zoom, FirstPageNumber, Order, BlackAndWhite, PrintQuality, HeaderMargin, FooterMargin, PrintComments, Draft)

このままだと使いにくいので、下にPageSetupExcel4Macro関数を用意しています。

実際にこれを使うことはまず無いので詳細は省略しますが、使い方は以下のような感じになります。

Sub PageSetupExcel4MacroTest()
    Call PageSetupExcel4Macro(LeftHeader:="abcdd")
End Sub

以下がPAGE.SETUPをそのまま実行するには使いにくいので各パラメータを省略可能な引数にして関数化したものです。過去には使っていましたが今は使うことはまずないと思います。設定値はApplication.PageSetupと同様なので省略します。

'// LeftHeader As String        :左ヘッダー
'// CenterHeader As String      :中央ヘッダー
'// RightHeader As String       :右ヘッダー
'// LeftFooter As String        :左フッター
'// CenterFooter As String      :中央フッター
'// RightFooter As String       :右フッター
'// LeftMargin As String        :左余白
'// RightMargin As String       :右余白
'// TopMargin As String         :上余白
'// BottomMargin As String      :下余白
'// HeaderMargin As String      :ヘッダー余白
'// FooterMargin As String      :フッター余白
'// PrintHeadings As String     :行列番号印刷
'// PrintGridlines As String    :枠線印刷
'// PrintComments As String     :コメント
'// PrintQuality As String      :印刷品質
'// CenterHorizontally As String:ページ中央水平
'// CenterVertically As String  :ページ中央垂直
'// Orientation As String       :用紙の向き
'// Draft As String             :簡易印刷
'// PaperSize As String         :用紙サイズ
'// FirstPageNumber As String   :先頭ページ番号
'// Order As String             :ページの方向
'// BlackAndWhite As String     :白黒印刷
'// Zoom As String)             :ズーム
Public Sub PageSetupExcel4Macro(Optional LeftHeader As String, Optional CenterHeader As String, Optional RightHeader As String, Optional LeftFooter As String, Optional CenterFooter As String, Optional RightFooter As String, Optional LeftMargin As String, Optional RightMargin As String, Optional TopMargin As String, Optional BottomMargin As String, Optional HeaderMargin As String, Optional FooterMargin As String, Optional PrintHeadings As String, Optional PrintGridlines As String, Optional PrintComments As String, Optional PrintQuality As String, Optional CenterHorizontally As String, Optional CenterVertically As String, Optional Orientation As String, Optional Draft As String, Optional PaperSize As String, Optional FirstPageNumber As String, Optional Order As String, Optional BlackAndWhite As String, Optional Zoom As String)
    Dim c           As String: c = ","  '// 項目区切り文字
    Dim sSetting    As String           '// 印刷設定
    Dim sHeader     As String           '// ヘッダ文字列
    Dim sFooter     As String           '// フッタ文字列
    
    '// ヘッダー設定
    sHeader = GetHeaderFooter(LeftHeader, CenterHeader, RightHeader)
    
    '// フッター設定
    sFooter = GetHeaderFooter(LeftFooter, CenterFooter, RightFooter)
    
    '// 印刷設定文字列をカンマで連結
    sSetting = sHeader & c & sFooter & c & LeftMargin & c & RightMargin & c & TopMargin & c & BottomMargin & c & PrintHeadings & c & PrintGridlines & c & CenterHorizontally & c & CenterVertically & c & Orientation & c & PaperSize & c & Zoom & c & FirstPageNumber & c & Order & c & BlackAndWhite & c & PrintQuality & c & HeaderMargin & c & FooterMargin & c & PrintComments & c & Draft
    
    '// 印刷設定を実行
    Call ExecuteExcel4Macro("PAGE.SETUP(" & sSetting & ")")
End Sub

Function GetHeaderFooter(a_sLeft As String, a_sCenter As String, a_sRight As String) As String
    Dim s   As String
    
    '// ヘッダ左が設定されている場合
    If a_sLeft <> "" Then
        s = "&L" & a_sLeft
    End If
    
    '// ヘッダ中央が設定されている場合
    If a_sCenter <> "" Then
        s = s & "&C" & a_sCenter
    End If
    
    '// ヘッダ右が設定されている場合
    If a_sRight <> "" Then
        s = s & "&R" & a_sRight
    End If
    
    '// ヘッダが設定されていない場合
    If Not s = "" Then
        s = """" & s & """"
    End If
    
    GetHeaderFooter = s
End Function

]]>
VBAの高速化(文字列の連結はJoinで行う) https://vbabeginner.net/speed-up-vba-join/ Tue, 27 Nov 2018 16:31:45 +0000 https://vbabeginner.net/?p=3740 文字列同士の連結は遅い

あまり知られていませんが、&や+での文字列の連結処理はかなり遅い処理になります。

その理由は、連結前の文字列と、連結後の文字列が格納されるメモリ領域が異なるためです。2つ以上の文字列を連結すると、連結した文字列を格納できるメモリ領域を新たに用意します。

メモリ領域の確保はコストが大きく、ループ処理で何度も文字列の連結を行うような場合には目に見えて遅くなります。それを回避するには、メモリ領域の確保が毎回必要な「文字列の連結」処理をやめるしかありません。

以下では、文字列の連結がどれぐらい遅いのか、そして、どうやったら速くなるのかをサンプルコードで説明します。

String型の連結速度(遅い方法)

実際にどれぐらい遅いのか計測してみます。

“a”という文字を30万回連結する処理です。

処理の前後にTimer関数を呼び出し、その差から処理秒数を算出しています。

Sub stringAppendTest()
    Dim s   As String
    Dim i
    Dim tm
    
    tm = Timer
    
    For i = 0 To 300000
        s = s & "a"
    Next
    
    tm = Timer - tm
    
    Debug.Print tm
End Sub

実行結果は平均すると17.6秒でした。

これが遅いかどうかを比較するために、単に”a”を30万回代入する処理をやってみます。

Sub stringSetTest()
    Dim s   As String
    Dim i
    Dim tm
    
    tm = Timer
    
    For i = 0 To 300000
        s = "a"
    Next
    
    tm = Timer - tm
    
    Debug.Print tm
End Sub

平均0.04秒です。

この結果からも、文字列の連結には時間が掛かることが分かります。

Join関数を使って連結する(速い方法)

先の処理ではループの度に&で連結していましたが、次のコードはループ処理では連結せずに1次元配列に格納して、ループを抜けたらJoin関数で連結する方法です。

なお、Join関数については「VBAで配列の全要素を連結して文字列にする(Join)」をご参照ください。

Sub stringJoinTest()
    Dim ar()    As String
    Dim i
    Dim s
    
    ReDim ar(300000)
    
    Debug.Print Timer
    
    For i = 0 To 300000
        ar(i) = "a"
    Next
    
    Debug.Print Timer
    
    s = Join(ar, "")

    Debug.Print Timer
End Sub

平均0.04秒です。

先の代入だけの速度とほとんど同じです。

ループ中は配列に代入し、ループ後にJoin関数を1度実行しただけのため、ほとんど同じなのも理解できます。

文字列の連結よりも圧倒的に速いことが分かります。

ただ、Redimで最初に30万の要素を確保しているのも速い理由ではないか、というのもあるので、次にループ中でRedim Preserveで配列の領域を拡張するコードにしてみます。

Sub stringJoinTest2()
    Dim ar()    As String
    Dim i
    Dim s
    
    ReDim ar(0)
    
    Debug.Print Timer
    
    For i = 0 To 300000
        ReDim Preserve ar(i)
        ar(i) = "a"
    Next
    
    Debug.Print Timer
    
    s = Join(ar, "")

    Debug.Print Timer
End Sub

平均0.2秒です。

配列の拡張に0.16秒ほど掛かっていると思われますが、それでも文字列の連結よりは速いことが分かります。

ただ、先のコードのようにRedimで事前に要素数を確保しておく方が速いので、可能であればそのようなコーディングをおすすめします。

まとめ

回数が多いループで文字列の連結を行う場合は配列に格納して、ループ後にJoin関数で連結するようにしましょう。

コードが複雑になることもありませんので、普段使いしてもいいと思います。

]]>
VBAの高速化(クラスは参照設定+Newする) https://vbabeginner.net/speed-up-vba-class-is-reference-setting-new/ Sun, 09 Jul 2017 15:22:28 +0000 http://vbabeginner.net/?p=448 参照設定を行いクラスの型宣言を行う方が速い

VBAでクラスを利用する場合に、変数宣言時に型を書く方法と書かない方法の2通りがあります。型を書く場合は、事前に参照設定で対象のライブラリにチェックをつけておく必要があります。そうすることで変数宣言時に型として指定することができます。

型を書かない場合は、変数名だけを書くか、もしくは実際のクラスの型ではなくObject型変数として定義することもできます。

いずれの場合も、変数宣言のあとでNewやCreateObject関数を利用して対象クラスのオブジェクトを生成する必要があります。

結論から言えば、参照設定を事前に行い、変数宣言時にデータ型としてクラスを指定した上で、対象クラスをNewでオブジェクト作成する方法が処理速度は一番速くなります。ただし、処理速度の差が体感できるのは100万件以上のような大量のデータを扱う場合になります。そうでない数回程度であればほとんど差はありません。

なお、参照設定はVBAの画面で、ツールメニュー→参照設定、でダイアログが開きます。

クラス利用時の型宣言は4通り

先に書いた内容をコードで示します。コードでは主に4つの書き方が出来ます。例としてFileSystemObjectを使います。他のクラスを使った場合も同じです。

以下の1つ目と2つ目の参照設定+型宣言がある書き方を事前バインディング、3つ目と4つ目のクラス型の型宣言をしない書き方を実行時バインディングや遅延バインディングと言います。

言葉自体はここでは気にしなくていいです。

クラスの型宣言+New

Dim fso As FileSystemObject
    Set fso = New FileSystemObject

クラスの型宣言+CreateObject

Dim fso As FileSystemObject
    Set fso = CreateObject("Scripting.FileSystemObject")

クラスの型宣言なし+CreateObject

Dim fso
    Set fso = CreateObject("Scripting.FileSystemObject")

Object型宣言+CreateObject

Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")

Object型の変数宣言ではオブジェクトが設定されることのみを定義しており、まだどういうクラスのオブジェクトが設定されるのか不明のため「クラスのオブジェクトが入る予定」の状態です。

そのため事前にはクラスが分かっていない状況です。

検証用ソースコード

実際にどれぐらいの差があるのかを検証します。

Sub VariableTypeSpeedTest2()
    Dim tmStart          As Double
    Dim tmEnd            As Double
    Dim tmDiff           As Double
    Dim fso1 As FileSystemObject
    Dim fso2 As FileSystemObject
    Dim fso3
    Dim fso4 As Object
    Dim i
    Dim b
    
    '// 1. 型宣言あり+New
    tmStart = Timer
    For i = 0 To 100000
        Set fso1 = New FileSystemObject
        b = fso1.FileExists("C:\aa.txt")
    Next
    tmEnd = Timer
    tmDiff = tmEnd - tmStart
    Debug.Print "1.型宣言あり+New:" & tmDiff & "秒"
    
    '// 2. 型宣言あり+CreateObject
    tmStart = Timer
    For i = 0 To 100000
        Set fso2 = CreateObject("Scripting.FileSystemObject")
        b = fso2.FileExists("C:\aa.txt")
    Next
    tmEnd = Timer
    tmDiff = tmEnd - tmStart
    Debug.Print "2.型宣言あり+CreateObject:" & tmDiff & "秒"
    
    '// 3. 型宣言なし+CreateObject
    tmStart = Timer
    For i = 0 To 100000
        Set fso3 = CreateObject("Scripting.FileSystemObject")
        b = fso3.FileExists("C:\aa.txt")
    Next
    tmEnd = Timer
    tmDiff = tmEnd - tmStart
    Debug.Print "3. 型宣言なし+CreateObject:" & tmDiff & "秒"
    
    '// 4. Object型宣言あり+CreateObject
    tmStart = Timer
    For i = 0 To 100000
        Set fso4 = CreateObject("Scripting.FileSystemObject")
        b = fso4.FileExists("C:\aa.txt")
    Next
    tmEnd = Timer
    tmDiff = tmEnd - tmStart
    Debug.Print "4. Object型宣言あり+CreateObject:" & tmDiff & "秒"
End Sub

実行結果(コンパイルせずに実行)
1. 型宣言あり+New:2.3125秒
2. 型宣言あり+CreateObject:3.1015625秒
3. 型宣言なし+CreateObject:3.703125秒
4. Object型宣言あり+CreateObject:3.6953125秒

この結果からも参照設定+型宣言ありの方が速いことが分かります。ただし、参照設定をしていてもNewとCreateObjectでは差が出ています。これはよく言われることですがCreateObject関数が遅いことが原因です。

CreateObjectは引数に文字列でクラス名を指定する必要があることからどうしても文字列処理が発生することや、オブジェクトを作成する先も指定可能なため処理が冗長化していることもあり、その分も遅延の原因になります。

結果として、型宣言がない場合ほどではありませんがかなりの遅延になっています。このことからも、参照宣言+型宣言あり+Newが一番処理速度は速いことが分かります。

Dim fso As FileSystemObject
    Set fso = New FileSystemObject

多くの場合はこの書き方での実装が可能と思われます。なお、この検証ではコンパイルを行わずに行っています。

事前バインディングについて「コンパイルを行うから速い」と説明しているサイトもありますが、それだけが速い理由ではなく上記の遅延原因を明示するためにあえてコンパイルしていません。

もちろんコンパイルするとさらに速くなります。以下はコンパイルを行った場合の実行結果ですが、Newの場合はコンパイルをしている方がしていないときよりも速くなっています。

それとは逆にCreateObjectを行うNo.2, 3, 4は、実行時に型判定されるためコンパイル有無が処理速度に影響していないことが分かります。

実行結果(コンパイルを行って実行)
1. 型宣言あり+New:2.18902587890625秒
2. 型宣言あり+CreateObject:3.06097412109375秒
3. 型宣言なし+CreateObject:3.69500732421875秒
4. Object型宣言あり+CreateObject:3.81500244140625秒

参照設定をした方が速い理由

事前バインディングと実行時バインディングについては以下のMicrosoftのヘルプに記載があります。

https://msdn.microsoft.com/ja-jp/library/cc343951.aspx

型宣言を事前に行った方が速いのは、事前バインディングと実行時バインディングの説明そのままになるのですが、型宣言をしていない場合は変数の型が実行時まで分からないことにより、処理の度に型判定を行うことになります。

型宣言を行っている場合は当然型判定は必要ありません。その差が遅延に繋がっています。

]]>
VBAの高速化(Selectでのセル選択を行わない) https://vbabeginner.net/speed-up-vba-do-not-select-cells-with-select/ Sat, 08 Jul 2017 19:58:50 +0000 http://vbabeginner.net/?p=443 Selectメソッドを使わない方が速い

RangeプロパティやCellsプロパティでセルを参照する際に、そのセルをSelectメソッドで選択することがあります。

選択しておいて、ActiveCellオブジェクトに対する各種メソッドやプロパティでの処理を行う場合などですね。

しかし、Selectをせずとも対象セルのメソッドやプロパティは利用可能です。

また、Selectをするのとしないのとではかなりの処理速度の差があります。

不要であればSelectは使わない

結論から言うと、Selectを使わない方が処理速度が速くなります

それも、劇的に速くなります

例えば、セルの値を参照するためにValueプロパティを使うことはよくある処理ですが、その際にセル選択のSelectをせずに直接Valueプロパティを利用すると処理速度が速くなります。

他の処理速度方法ではコードが長くなったり、可読性が落ちるなどの弊害も出てくることがあるのですが、この改善方法はコード量も減りますし、可読性も低下しません。

テストコードで検証

以下のソースで実測してみました。

10000回セルの参照を行うテストです。

1つはSelectを行ってValueを取得するパターンで、もう1つはSelectを行わずにValueを取得するパターンです。

Sub RangeCellsOffsetCellSelectTest()
    Dim tmStart          As Double
    Dim tmEnd            As Double
    Dim tmDiff           As Double
    Dim i                As Long
    Dim s
    
    '// 処理前の時間を取得
    tmStart = Timer
    '// 計測対象の処理
    For i = 1 To 10000
        Cells(i, 1).Select
        s = ActiveCell.Value
    Next
    '// 処理後の時間を取得
    tmEnd = Timer
    tmDiff = tmEnd - tmStart
    Debug.Print "Selectあり:" & tmDiff & "秒"
    
    '// 処理前の時間を取得
    tmStart = Timer
    '// 計測対象の処理
    For i = 1 To 10000
        s = Cells(i, 1).Value
    Next
    '// 処理後の時間を取得
    tmEnd = Timer
    
    '// 処理前後の差を取得
    tmDiff = tmEnd - tmStart
    Debug.Print "Selectなし:" & tmDiff & "秒"
End Sub

 

検証結果

上記のテストコードの実行結果は以下の通りです。

Selectあり:3.8466796875秒
Selectなし:0.0380859375秒

数値上はSelectの有無で100倍以上の差があることが分かりますが、100倍というよりも、直接Valueで指定すると即時処理が終わっている、と見た方がいいでしょうね。

Selectすると遅い理由

Selectすることで遅くなる理由は、画面描画やイベント処理に時間が掛かっていることと、ActiveCellオブジェクトなどの選択セルの状態の書き換えに時間が掛かることが原因と思われます。

以下は画面描画などのApplication.ScreenUpdatingなどを無効にした場合のテストコードです。

Sub RangeCellsOffsetCellSelectTestB()
    Dim tmStart          As Double
    Dim tmEnd            As Double
    Dim tmDiff           As Double
    Dim i                   As Long
    Dim s
    
    With Application
        .ScreenUpdating = False
        .Calculation = xlCalculationManual
        .EnableEvents = False
        .PrintCommunication = False
    End With
    
    '// 処理前の時間を取得
    tmStart = Timer
    '// 計測対象の処理
    For i = 1 To 10000
        Cells(i, 1).Select
        s = ActiveCell.Value
    Next
    '// 処理後の時間を取得
    tmEnd = Timer
    tmDiff = tmEnd - tmStart
    Debug.Print "Selectあり:" & tmDiff & "秒"
    
    '// 処理前の時間を取得
    tmStart = Timer
    '// 計測対象の処理
    For i = 1 To 10000
        s = Cells(i, 1).Value
    Next
    '// 処理後の時間を取得
    tmEnd = Timer
    
    '// 処理前後の差を取得
    tmDiff = tmEnd - tmStart
    Debug.Print "Selectなし:" & tmDiff & "秒"
    
    With Application
        .ScreenUpdating = True
        .Calculation = xlCalculationAutomatic
        .EnableEvents = True
        .PrintCommunication = True
    End With
End Sub

実行結果
Selectあり:0.236328125秒
Selectなし:0.041015625秒

確かにApplicationのプロパティを無効にすると速くはなりますが、それでもActiveCellの書き換えを行う分、Selectしている方が遅いです。でもまあここまでくればOKでしょうけどね。

]]>
VBAの高速化(RangeとCellsの使い分け) https://vbabeginner.net/speeding-up-vba-selecting-range-and-cells/ Fri, 07 Jul 2017 19:43:47 +0000 http://vbabeginner.net/?p=437 セル参照のRangeとCellsは使い分けをした方がよいが、、

VBAでセルを参照する際にRangeプロパティかCellsプロパティのどちらかを使うことになります。

その際に、Cellsプロパティだけで実装することも、Rangeプロパティだけで実装することも可能です。

しかし、このようにかなり融通は利くのですが、状況に応じて適切に使い分けを行うことで処理速度が変わってきます

ただ、そんなに変わりません。

単一セルはCells、複数セルはRangeを使うと速い

結論から言うと、単一セルの場合はCellsプロパティを使い、複数セルの場合はRangeを使うようにするのが一番処理速度は速いです。

ただし、劇的に速くなるようなことにはなりません。

各条件ごとに速度の違いについて後述します。

ただし、処理速度が一番速い書き方にこだわる必要はない、というのが私の考えです。

それについても後述します。

RangeとCellsの使い分け

RangeプロパティとCellsプロパティは厳密に用途が異なります。

Rangeプロパティの用途

Rangeプロパティは以下の2つの用途で使います。

・単一セルをA1形式で参照する場合(Range(“A1”))
・複数セルやセル範囲を参照する場合(A1~B2セルを参照:Range(“A1:B2”)、A~C列を参照:Range(“A:C”)、A1~B2範囲とC3~D4範囲を参照:Range(“A2:B2,C3:D4”))

Cellsプロパティの用途

・単一セルを行番号と列番号を指定して参照する場合(A2セルを参照:Cells(2, 1))

単一セルの参照はCellsの方が速い

単一セルを参照する場合は、Cellsプロパティの方が速いです。

その理由ですが、座標の文字列変換があるかないかの違いと思われます。

Cellsプロパティは行番号と列番号を数値で指定するため、数値がそのままシートのセル座標に置き換えて処理が可能ですが、Rangeプロパティは、”A1″という文字列でセル位置を指定するため、英字部分と数字部分に切り分けて、英字から列番号位置を算出して、行番号文字列と列番号も列をそれぞれ数値に変換してセル座標の置き換え、という流れを行っているものと思われます。

この変換処理の分、RangeプロパティはCellsプロパティよりも時間が掛かります。

以下のソースで実測してみました。

A1セルを参照する処理をRangeとCellsのそれぞれ1万回行っています。

計測には高精度時間を算出する関数を利用しています。

'// 64bit版
Type LARGE_INTEGER
    LowPart As Long
    HighPart As Long
End Type
#If VBA7 And Win64 Then
    Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As LARGE_INTEGER) As Long
    Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As LARGE_INTEGER) As Long
'// 32bit版
#Else
    Declare Function QueryPerformanceFrequency Lib "kernel32" (frequency As Double) As Long
    Declare Function QueryPerformanceCounter Lib "kernel32" (procTime As Double) As Long
#End If

'// 引数:Excelブックのフルパスを指定する
Function GetMicroSecondEx(frequency As Double) As Double
    Dim procTime            As Double       '// 高分解能パフォーマンスカウンタ値(システム起動からの加算値)
    Dim ret                 As Double       '// 計測結果
    
    '// 計測時刻を0で初期化
    GetMicroSecondEx = 0

    '// 処理時刻を取得
    Call QueryPerformanceCounter(procTime)

    '// カウンタ値を1秒間のカウント増加数で割り、正確な時刻を算出
    GetMicroSecondEx = procTime / frequency
End Function

'// 引数:Excelブックのフルパスを指定する
Sub RangeCellsSelectSpeedTest()
    Dim rangeStart          As Double
    Dim rangeEnd            As Double
    Dim rangeDiff           As Double
    Dim cellsStart          As Double
    Dim cellsEnd            As Double
    Dim cellsDiff           As Double
    Dim frequency           As Double
    Dim i                   As Long
    
    '// 更新頻度を取得
    Call QueryPerformanceFrequency(frequency)
    
    '// 処理前の時間を取得
    rangeStart = GetMicroSecondEx(frequency)
    '// 計測対象の処理
    For i = 1 To 10000
        Range("A1").Select
    Next
    '// 処理後の時間を取得
    rangeEnd = GetMicroSecondEx(frequency)
    
    '// 処理前の時間を取得
    cellsStart = GetMicroSecondEx(frequency)
    '// 計測対象の処理
    For i = 1 To 10000
        Cells(1, 1).Select
    Next
    '// 処理後の時間を取得
    cellsEnd = GetMicroSecondEx(frequency)
    
    
    '// 処理前後の差を取得
    rangeDiff = rangeEnd - rangeStart
    cellsDiff = cellsEnd - cellsStart
    
    Debug.Print "Range:" & rangeDiff & "秒"
    Debug.Print "Cells:" & cellsDiff & "秒"
End Sub

実測結果は、Rangeが1.5秒、Cellsが1.44秒と、Cellsの方が若干速いです。

Range:1.50643464864697秒
Cells:1.44152700668201秒

Range(“A” & CStr(iRow))と書くと遅い

例えばRange(“A1”)からRange(“A100000”)までのループ処理で、1から100000の部分をループカウンタで代用することがあります。

こんな書き方ですね。

'// 引数:Excelブックのフルパスを指定する
Sub Range1To100000()
    Dim iRow
    
    '// 1から100000までループ
    For iRow = 1 To 100000
        '// 指定セルを選択
        Range("A" & CStr(iRow)).Select
    Next
End Sub

先に書いた内容と同じですが、Rangeの場合は構文解析が行われることにより処理速度が遅くなります。

以下のようにCellsに書き換えた方が処理速度は速くなります。

'// 引数:Excelブックのフルパスを指定する
Sub Cells1To100000()
    Dim iRow
    
    '// 1から100000までループ
    For iRow = 1 To 100000
        '// 指定セルを選択
        Cells(iRow, 1).Select
    Next
End Sub

どれぐらい速くなるのか確認してみます。

'// 引数:Excelブックのフルパスを指定する
Sub RangeCellsSpeedTest()
    Dim rangeStart          As Double
    Dim rangeEnd            As Double
    Dim rangeDiff           As Double
    Dim cellsStart          As Double
    Dim cellsEnd            As Double
    Dim cellsDiff           As Double
    Dim frequency           As Double
    
    '// 更新頻度を取得
    Call QueryPerformanceFrequency(frequency)
    
    '// 処理前の時間を取得
    rangeStart = GetMicroSecondEx(frequency)
    '// 計測対象の処理
    Call Range1To100000
    '// 処理後の時間を取得
    rangeEnd = GetMicroSecondEx(frequency)
    
    '// 処理前の時間を取得
    cellsStart = GetMicroSecondEx(frequency)
    '// 計測対象の処理
    Call Cells1To100000
    '// 処理後の時間を取得
    cellsEnd = GetMicroSecondEx(frequency)
    
    
    '// 処理前後の差を取得
    rangeDiff = rangeEnd - rangeStart
    cellsDiff = cellsEnd - cellsStart
    
    Debug.Print "Range:" & rangeDiff & "秒"
    Debug.Print "Cells:" & cellsDiff & "秒"
    
End Sub

実行結果はこうなりました。

Range:15.3357434456702秒
Cells:11.3562810394214秒

ループカウンタの100000を10000に変更すると実行結果はこうなりました。

Range:3.32276758423541秒
Cells:2.35987033683341秒

単純には言えませんが、Cellsに比べて1.4倍程度Rangeの方が時間が掛かっています。

Range(“xx”).Offsetは遅い

私もよく使うOffsetですが、Cellsと比べると処理は遅いです。

基点からの指定行列位置を求める必要があるため、直接アドレスを指定するCellsよりも遅くなります。

これも実測してみます。

'// 引数:Excelブックのフルパスを指定する
Sub RangeCellsOffsetSpeedTest()
    Dim rangeStart          As Double
    Dim rangeEnd            As Double
    Dim rangeDiff           As Double
    Dim cellsStart          As Double
    Dim cellsEnd            As Double
    Dim cellsDiff           As Double
    Dim frequency           As Double
    Dim i                   As Long
    
    '// 更新頻度を取得
    Call QueryPerformanceFrequency(frequency)
    
    '// 処理前の時間を取得
    rangeStart = GetMicroSecondEx(frequency)
    '// 計測対象の処理
    For i = 1 To 10000
        Range("A1").Offset(i, 0).Select
    Next
    '// 処理後の時間を取得
    rangeEnd = GetMicroSecondEx(frequency)
    
    '// 処理前の時間を取得
    cellsStart = GetMicroSecondEx(frequency)
    '// 計測対象の処理
    For i = 1 To 10000
        Cells(i, 1).Select
    Next
    '// 処理後の時間を取得
    cellsEnd = GetMicroSecondEx(frequency)
    
    
    '// 処理前後の差を取得
    rangeDiff = rangeEnd - rangeStart
    cellsDiff = cellsEnd - cellsStart
    
    Debug.Print "Range:" & rangeDiff & "秒"
    Debug.Print "Cells:" & cellsDiff & "秒"
End Sub

実測結果です。

Cellsに比べて1.65倍ほどRangeの方が時間が掛かっています。

Range:3.71202648698818秒
Cells:2.24162556300871秒

でも、単一セルにRangeやOffsetを使っても構わない

処理速度の向上を目的として書いた記事ではありますが、個人的にはRangeとCellsに関して言えば「慣れている書き方」や「好きな書き方」を優先させて処理速度を犠牲にしても構わないと思っています。

この記事で言えば、単一セルやOffsetでRangeは使わずにCellsを使った方が処理速度は速くなります、と書いてますが、単一セルやOffsetでRangeを使っても全然いいと思います。

現に私はCellsの方が速いと分かっていても、マクロを作る際にRange(“A” & CStr(i))やOffsetを多用します

VBAに限らずあらゆるプログラミング言語で処理速度向上を行う方法がありますが、個人的には処理速度向上は目的に対する優先度としてはそんなに高くありません

ここで言う目的とは、以下に挙げるようなマクロを作って行う何らかの自動化、を指します。

  • 1回しか動かさないマクロなんで処理速度とかどうでもいい。
  • ループが10回程度なんでRangeでもCellsでもほとんど変わらないし気にもしない。
  • Range(“A” & CStr(i))と書く方がA列なのがはっきりして分かりやすい。
  • Offsetを使った方が紙の資料と見比べながらマクロを書く際に分かりやすい

などなど、コードを書くときの状況やコードを書いた人の考え方はバラバラです。

その状況によって何が優先されるのかは変わりますので、ガチガチに「処理速度が速い方法で書け!」なんてことを言ってるのは違うと思います。

そして上でも実測結果を挙げましたが、速いといってもある程度、というぐらいのものです。ついでに言えば、数秒程度の差であればパソコンが新しくなるだけで差も小さくなっていきます。

処理速度をより速くするのは、それ自体は良いことですが、それよりも大事なことがある場合はそちらを優先しましょう

]]>