Visual Basic 中学校 掲示板 投稿内容
タグのない投稿を抽出 統計 RSS

Visual Basic 中学校 > 投稿一覧 >

vb6 VSPrinterでの処理スピードが遅い 解決済み

タグの編集...

投稿者 はなな   (社会人)   投稿日時 2022/5/16 11:32:09
vb6でVSPrinterを使用していますが、表示までのスピードが遅いです。

調べてみると、遅いロジックが下記のロジックということが分かりました。
たぶん、たくさんのデータを1つづつセットしていることが原因だとは思うのですが
どのように改善したらよいのかが分かりません。

データの読み込み(約600件)には0.06秒しかかからないのに
下記のロジック(1件の内容を1ページに出力×600件)は8.6秒もかかります。

なにか改善点があれば教えて頂けると助かります。
どうぞよろしくお願い致します。


Dim Page As Long
Dim PrintPage As Long

Dim x(20)   As Long
Dim y(20)   As Long

x(0) = 8
x(1) = 8

・ 20個セット
y(0) = 7

・ 20個セット

    With VSPrinter1
        .Zoom = 100
        .Preview = True
        .FontName = "MS 明朝"
        .PaperSize = pprA4       
        .Orientation = orLandscape
        .FontSize = 11
        
        .StartDoc
        
        .PenWidth = 1
        .PenStyle = psSolid

        PrintPage = 0
        
        For Page = 0 To データ数(約600件) - 1
                If PrintPage <> 0 Then .NewPage
                PrintPage = PrintPage + 1


                '横線
                For i = 2 To 15
                    .DrawLine x(1), y(i), x(18), y(i)
                Next
                
                '縦線 → 20個ほど記述
                .DrawLine x(2), y(9), x(2), y(15)
      

                .FontSize = 24 
                Call PrintText("印字内容1", x(1), y(1) + 140, x(18) - x(1),20, 1)
                Call PrintText("印字内容2", x(2), y(2) + 140, x(18) - x(1),20, 1)

                .FontSize = 8
                Call PrintText("印字内容3", x(3), y(3) + 140, x(18) - x(1),20, 1)
                Call PrintText("印字内容4", x(4), y(4) + 140, x(18) - x(1),20, 1)

          ’FontSize を変えてPrintTextで印刷内容を100項目程セットする
        Next
        
        .EndDoc
    
    End With

Private Sub PrintText(s As String, dx As Long, dy As Long, wl As Long, ks As Long, Mode As Integer)
    Dim s1          As String
    Dim strData()       As String
    Dim i               As Integer
    
    If s = "" Then Exit Sub
    If wl < 0 Then Exit Sub
    s1 = s
    
    strData = Split(s1, vbCrLf)
    If UBound(strData) > 0 Then
        s1 = ""
        For i = 0 To UBound(strData)
            If s1 <> "" Then s1 = s1 & ".."
            s1 = s1 & strData(i)
        Next
    End If
    
    With VSPrinter1
        .Measure = s1
        Select Case Mode
            Case W_CENTER
                .CurrentX = (dx + dx + wl) / 2 - .TextWid / 2
                If .CurrentX < dx Then
                    .CurrentX = dx
                End If
            Case W_LEFT
                .CurrentX = dx
            Case W_RIGHT
                .CurrentX = dx + wl - .TextWid - 30
        End Select
        .CurrentY = (dy + dy + ks) / 2 - .TextHei / 2
        While .TextWid > wl
            s1 = Left(s1, Len(s1) - 1)
            .Measure = s1
        Wend
        .Text = s1
    End With
End Sub

投稿者 (削除されました)   ()   投稿日時 2022/5/16 13:49:45
(削除されました)

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2022/5/16 13:56:10
> データの読み込み(約600件)には0.06秒しかかからないのに
> 下記のロジック(1件の内容を1ページに出力×600件)は8.6秒もかかります。

たとえば、Sub PrintText 内の「VSPrint に受け渡す前準備の処理」に
多くの時間が割かれているという可能性は無いですか?

仮に、PrintText の処理が、一回あたり 0.015 秒かかるとしたら、
0.015 秒 × 600 回=9.0 秒という計算になりますよね。


特に気になるのはココ。

> strData = Split(s1, vbCrLf)
> s1 = ""
> For i = 0 To UBound(strData)
>   If s1 <> "" Then s1 = s1 & ".."
>   s1 = s1 & strData(i)
> Next

これはループせずとも、Join だけで済むように見えます。

strData = Split(s1, vbCrLf)
s1 = Join(strData, "..")



実際の繰り返し回数は、strData の内容に左右されますが、ここでは検査のために
あえて 10 万回という大量データで比較してみます。

ReDim strData(0 To 99999) As String
For i = LBound(strData) To UBound(strData)
    strData(i) = String(20, "*")
Next


上記の配列を、はななさんのループ処理で再結合すると 140 秒かかりましたが、
それを Join 結合法に差し換えると、僅か 0.004 秒で同じ結果が得られました。


さらに言えば、そもそも Split すら不要な気がします。

s1 = Replace(s1, vbCrLf, "..")


投稿者 魔界の仮面弁士   (社会人)   投稿日時 2022/5/16 14:37:17
> While .TextWid > wl
>  s1 = Left(s1, Len(s1) - 1)
>  .Measure = s1
> Wend

これも繰り返し処理(ループ内のループ)なので、
場合によっては遅くなる要因になるかもしれません。

VSPrint は使ったことが無いですが、処理の内容から想像してみると、
領域の幅におさまるように文字列の末尾を切り捨てる処理でしょうか?


ボトルネックの箇所を特定するために、それぞれの部分が
どの程度の時間がかかっているのかを、さらに調査してみてください。


Dim tmr1 As Single
tmr1 = Timer1

' ここに時間のかかる処理を記述 

Debug.Print Timer - tmr1



上記は Timer を用いた簡易的な測定方法です。

Timer で測定できる精度は、せいぜい 1/256秒(3.90625ミリ秒)程度ですので、
一回当たりの処理が短時間で完了する場合は、 結果が 0 になってしまうかもしれません。

その場合は下記を使うこともできます。
https://hatenachips.blog.fc2.com/blog-entry-377.html

投稿者 はなな   (社会人)   投稿日時 2022/5/16 15:49:46
魔界の仮面弁士さん、ありがとうございます。

>たとえば、Sub PrintText 内の「VSPrint に受け渡す前準備の処理」に
>多くの時間が割かれているという可能性は無いですか?

はい、この部分のみで8.6秒かかってしまうのです。


>さらに言えば、そもそも Split すら不要な気がします。
>s1 = Replace(s1, vbCrLf, "..")

ほんとうにそうですね!
全く気づきませんでしたので、さっそくこのループ処理を確認してみたところ
”vbCrLf”が含まれるデータは1つもなく、このループ処理に入る事はありませんでしたので、
ここが原因ではないようです。
しかし、これからのことを考え”Replace”に変更しておきました。



>> While .TextWid > wl
>>  s1 = Left(s1, Len(s1) - 1)
>>  .Measure = s1
>> Wend
>
>これも繰り返し処理(ループ内のループ)なので、
>場合によっては遅くなる要因になるかもしれません。
>
>VSPrint は使ったことが無いですが、処理の内容から想像してみると、
>領域の幅におさまるように文字列の末尾を切り捨てる処理でしょうか?

おっしゃるとおりです。
試しにこのループ処理をレム文にして実行してみましたが、処理速度は変わりませんでした。
調べてみると、実際にこのループ処理に入ってくるのは、600件×100項目のうちたった3回でした。


この2点では処理速度は変わらなかったのですが
全く私には思いもつかないところでしたので、魔界の仮面弁士さんの言うように
もっとピンポイントでボトルネックの箇所を特定しないといけないということですね。

そこが分かれば解決策ももっと具体的に分かりますもんね。
もう少し調べてみます。

もし他に気になるところがあったら、ぜひ教えて下さい。
よろしくお願い致します。

投稿者 はなな   (社会人)   投稿日時 2022/5/16 16:38:15
試しに100項目あったPrintTextを半分にして実行してみました。
4.2秒ほどになりました。
もちろん項目数を減らすことは出来ないので、PrintTextの処理スピードを少しでも上げることが出来ればいいのですが...


投稿者 魔界の仮面弁士   (社会人)   投稿日時 2022/5/16 16:40:14
>> たとえば、Sub PrintText 内の「VSPrint に受け渡す前準備の処理」に
>> 多くの時間が割かれているという可能性は無いですか?
> はい、この部分のみで8.6秒かかってしまうのです。

……え?
Sub PrintText 内で With VSPrinter1 を呼び出す前の箇所というのは、下記の数行ですよね。

If s = "" Then Exit Sub
If wl < 0 Then Exit Sub
s1 = s

strData = Split(s1, vbCrLf)
If UBound(strData) > 0 Then
    s1 = ""
    For i = 0 To UBound(strData)
        If s1 <> "" Then s1 = s1 & ".."
        s1 = s1 & strData(i)
    Next
End If



> ”vbCrLf”が含まれるデータは1つもなく、このループ処理に入る事はありませんでしたので、
上記のうち、「ループ以外の箇所」となると、単純な If 文しかなさそうですが、
それで 8.6秒かかってしまうのですか…。今回の時間計測はどの範囲で行われていますか?

もしも Sub PrintText 内ではなく、呼び出し側の
>> With VSPrinter1
>>    .Zoom = 100
も含めての時間だというのであれば、ボトルネック箇所をもう少し仔細に調べてみてください。


> もし他に気になるところがあったら、ぜひ教えて下さい。
VSPrinter の特性を知らないので、以下、全て机上論になってしまいますが:

仮説Ⓐ 特定のデータを含むレコードに至った時に、低速化している可能性

仮説Ⓑ VSPrinter1 側の問題ではなく、プリンタードライバー側の問題により、
 特定のプロパティの読み取り(あるいは書き取り)が、ほんの少し遅い状態である


Ⓐについては、たとえば 600 回の「For Page」ループを 6 分割して、
 Page =   0~ 99
 Page = 100~199
 Page = 200~299
 Page = 300~399
 Page = 400~499
 Page = 500~599
の範囲それぞれにかかる時間を計測してみればわかるかもしれません。

極端な差が生じていないようであれば、データ依存性の可能性は排除できるでしょう。

Ⓑについては、たとえば、何らかのプロパティの操作 1 回当たりの処理時間が
 1.5 ミリ秒かかるドライバーと
 15 ミリ秒かかるドライバーがあったと仮定してみます。

一回当たりの差は体感不能なほど僅かですが、それを 500 回繰り返せば、
前者は 0.8 秒未満でも、後者は7秒半かかる計算ですよね。

この件については、別メーカーのプリンタードライバーで試してみることで判断できます。
実際のプリンターは無くても良いので、ドライバーだけインストールして、
出力ポートをダミーポート(FILE: ポートや NUL: ポート)にするか、あるいは
既存ポート(LPT1: とかネットワークポート)を割り当てつつ、オフライン指定すれば試せます。

この時、キヤノン、エプソン、NEC、ブラザー等々から提供される純正ドライバーの他、
OS に標準で付いている汎用ドライバーとも比較してみてください。


プログラム的に言えば、たとえば W_CENTER モードの CurrentX プロパティは、
> .CurrentX = (dx + dx + wl) / 2 - .TextWid / 2
> If .CurrentX < dx Then
>   .CurrentX = dx
> End If
よりも下記の方が、プロパティへのアクセス回数を低減できます。
    newX = (dx + dx + wl) / 2 - .TextWid / 2
    If newX < dx Then
        .CurrentX = dx
    Else
        .CurrentX = newX
    End If


また、CurrentX プロパティおよび TextWid プロパティのデータ型が何であるかも確認を。
Integer なのか Long なのか、Single なのか Double なのか?

両方が Dougle 型であるのならば元の算出式で良いですが、もしも Long 型なのであれば、
  newX = (dx + dx + wl) / 2 - .TextWid / 2
ではなく、
  newX = (dx + dx + wl - .TextWid) \ 2&
 の方が良いでしょうし、両者が Single である場合は
  newX = (CSng(dx + dx + wl) - .TextWid) / 2.0!
 とするのが、データ型的に正しそうです。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2022/5/16 16:59:21
VSPrinter のバージョンが分かりませんが、8.0 のマニュアルを見た限りでは、
Measure、TextWid、TextHei の利用は非推奨機能になっていますね。
https://help.grapecity.com/componentone/NetHelp/vsview8/measureproperty.html

現在はかわりに、TextWidth と TextHeight を使うことになっているようで。
https://help.grapecity.com/componentone/NetHelp/vsview8/textwidthpropertyvsprinter.html


> Select Case Mode
>  Case W_CENTER
呼び出し側が「W_CENTER」「W_LEFT」「W_RIGHT」を用いずに
マジックナンバー「1」を使うのは対称性が悪くなりますね。
処理時間とは別件ですが、こういう場合は Public Enum を設けて、
As Integer ではなく列挙型で指定する形の方が良いかも。


> 試しに100項目あったPrintTextを半分にして実行してみました。

PrintText 内のコードを半分にしたのではなく、
PrintText の呼び出し回数を半分にした、ということでしょうか。


> 4.2秒ほどになりました。
・DrawLine するだけの処理にしてみる
・ループ内で FontSize を変更せずに時間計測してみる
・CurrentX , CurrentY を設定した後、TextWid や TextHei による再調整を一切行わなかった場合と時間を比較してみる

投稿者 はなな   (社会人)   投稿日時 2022/5/17 09:59:10
魔界の仮面弁士さん、ありがとうございます。

>> たとえば、Sub PrintText 内の「VSPrint に受け渡す前準備の処理」に
>> 多くの時間が割かれているという可能性は無いですか?
> はい、この部分のみで8.6秒かかってしまうのです。

すみません、8.6秒かかるのは、yを宣言した後から、EndDocの後の End With まででした。
要するに約600件全てを処理する時間です。

>Dim y(20)   As Long
↑ここから
>        .EndDoc
>    
>    End With
↑ここまで


仮説Ⓐについて調べてみました。
600件のうちの1件を処理する時間が、0.015秒から0.031秒と振れ幅があることが分かりました。
(For Page = 0 To データ数(約600件) - 1  から
 Next) まで
どういったデータのときに0.031秒かかっているのか、もう少しこの部分を調査してみます。


>> Select Case Mode
>>  Case W_CENTER
>呼び出し側が「W_CENTER」「W_LEFT」「W_RIGHT」を用いずに
>マジックナンバー「1」を使うのは対称性が悪くなりますね。
すみません、この部分は呼び出し側でも「W_CENTER」「W_LEFT」「W_RIGHT」を使用しています。
こちらに書く際に直してしまったようです。


>> 試しに100項目あったPrintTextを半分にして実行してみました。
>
>PrintText 内のコードを半分にしたのではなく、
>PrintText の呼び出し回数を半分にした、ということでしょうか。
はい、呼び出し回数を半分にした、ということです。(100項目を50項目にした)


>・DrawLine するだけの処理にしてみる
0.115秒でした。

>・ループ内で FontSize を変更せずに時間計測してみる
8.15秒でした。

>・CurrentX , CurrentY を設定した後、TextWid や TextHei による再調整を一切行わなかった場合と時間を比較してみる
8.4秒でした。


>プログラム的に言えば、たとえば W_CENTER モードの CurrentX プロパティは、
>> .CurrentX = (dx + dx + wl) / 2 - .TextWid / 2
>> If .CurrentX < dx Then
>>   .CurrentX = dx
>> End If
>よりも下記の方が、プロパティへのアクセス回数を低減できます。
なるほど、そういった積み重ねで時間を取られているのかも知れませんね。
そういったところが他にないかも確認してみます。
こちらの変更をしたところ、8.57秒でした。



投稿者 魔界の仮面弁士   (社会人)   投稿日時 2022/5/17 15:14:46
> 呼び出し回数を半分にした、ということです。(100項目を50項目にした)
StartDoc 前後の事前処理や、末尾の EndDoc にかかる時間が
無視できるほど小さい場合、単純にループ部のみの時間であると仮定して:

600件×(PrintTextの時間×100項目)≒8.6秒
600件×(PrintTextの時間× 50項目)≒4.2秒

∴ PrintTextの時間 ≒ 0.000146667 秒 (146.667マイクロ秒)


なるほど。100 項目ならばおよそ 0.0147 秒/件 ですね。


> 600件のうちの1件を処理する時間が、0.015秒から0.031秒と振れ幅があることが分かりました。
時間の計測分解能が 1/256 秒単位だと仮定すると、4/256秒~8/256秒といったところですね。

1 件のデータを 4/256秒 程度で出力した場合、600 件で 9.38秒程度かかる計算。
2 件のデータを 3/256秒 程度で出力できれば、600 件で 3.52秒程度かかる計算。
7 件のデータを 9/256秒 程度で出力できれば、600 件で 3.01秒程度かかる計算。


ちなみに Timer による計測精度は環境によって異なるそうです。

API だと、timeGetTime の分解能が 1 ミリ秒で、
GetTickCount の既定の分解能が 15.6 ミリ秒と聞いていますが、
呼び出してから結果が返ってくるまでの時間は、
GetTickCount より timeGetTime の方が少し遅いそうな。

処理時間がナノ秒やミリ秒の精度の場合は、上記いずれでも正確に測ることはできないので、
同じ処理を何十回、何百回、何千回と繰り返してみて、そのトータル時間を割って
一回時間の平均を求めることになりますね。


> どういったデータのときに0.031秒かかっているのか、もう少しこの部分を調査してみます。
その揺れに再現性があるかどうかが争点になってきます。
データによる再現性が無いのであれば、データ依存では無くただの計測誤差ということに。


>> 下記の方が、プロパティへのアクセス回数を低減できます。
> なるほど、そういった積み重ねで時間を取られているのかも知れませんね。
大した差にはならないかもしれませんし、大きな差に繋がるかもしれません。
計測してみないと分からないですね。


PrintText 内で VSPrinter に対して下記の 7 機能を呼び出していますが、
その中のどの処理が、処理時間の何パーセントを占めているのかを調べ、
そもそもの性能限界を確認しておいた方が良いかもしれません。

[PRE](1) Measure  プロパティ(代入)
(2) TextWid  プロパティ(取得)
(3) TextHei  プロパティ(取得)
(4) CurrentX プロパティ(代入)
(5) CurrentX プロパティ(取得)
(6) CurrentY プロパティ(代入)
(7) Text     プロパティ(代入)[/CODE]

たとえば (7) が、PrintText 内の処理時間の 8 割以上を締めている場合、
恐らくは大きな速度軽減は望めないでしょう。

一方、(1)~(3)が処理時間の 3割以上を占めていたとすれば、
あらかじめ (4), (6) に渡すべき位置情報をデータとして持たせる仕組みを設けることで
処理時間を 1 秒以上短縮できるかもしれません。


それ以上を求める場合は、VSPrinter を使う代わりに、
VB6 の Printer オブジェクトで出力した場合や、
API の TextOut 関数で出力した場合と比較してみるという
大工事も視野に入れなければならないかもしれません。検証しないとわかりませんが。

投稿者 はなな   (社会人)   投稿日時 2022/5/18 13:41:49
魔界の仮面弁士さん、ありがとうございます。

>ちなみに Timer による計測精度は環境によって異なるそうです。
計測精度について、詳しい説明をいただきありがとうございます。

>> どういったデータのときに0.031秒かかっているのか、もう少しこの部分を調査してみます。
>その揺れに再現性があるかどうかが争点になってきます。
>データによる再現性が無いのであれば、データ依存では無くただの計測誤差ということに。
データによる再現性がありませんでした。
また、同じデータを100回繰り返し実行してみたのですが、1件ごとの計測にばらつきがありました。
なので、ただの計測誤差のようです。

>たとえば (7) が、PrintText 内の処理時間の 8 割以上を締めている場合、
>恐らくは大きな速度軽減は望めないでしょう。
残念なことに、(7) が8割以上を占めていました。

>大工事も視野に入れなければならないかもしれません。検証しないとわかりませんが。
大工事は今のところ無理なので、この環境ではどうしようもない、という結論に至りました。

結果としてはスピードアップは図れませんでしたが
色々と詳しく教えて頂き、とても勉強になりました。
本当にありがとうございました。


投稿者 はなな   (社会人)   投稿日時 2022/5/18 13:44:02
解決チェックを忘れました