プログラム講座 中級編27

- ボトルネック -

 中級編27です。今回は「ボトルネック」について説明します。




ボトルネック
 Future BASICはBASICの中でもかなり高速な部類に入ります。しかし、プログラムの作り方によっては、大幅に実行速度が異なります。プログラムの遅い部分(ボトルネック)が、あると他が速くても実行速度が大幅に低下してしまいます。
 今回は2万個(以上)のデータを処理し、実際にどのくらい差が出るのか調べてみることにしましょう。処理する内容は以下の通りです。


 今回は先に結果を載せておきます。

powerPC750/233MHz激速(inputW)0.35 sec
高速(inputX)4.05 sec
普通(inputN)8.4 sec
68040/25MHz激速(inputW)1.6 sec
高速(inputX)45 sec
普通(inputN)114 sec

 見ての通り、激速版と高速版でさえ10倍近い速度差が出てしまっています。それぞれ、どのような処理を行っているのか説明します。



普通 [inputN関数部分](INPUT#,PRINT#を使うタイプ)
 ファイルの読み書きを行う場合、最も手軽に行えるのがINPUT#命令とPRINT#命令です。ただし255文字までしか扱えないという制限があります。
 ファイルを読み込み、加工して書き出す部分は以下のプログラム部分です。

  WHILE NOT EOF(1)
    INPUT #1,dt$:                                 ' "カンマまたは改行まで読み込む"
    PRINT #2,dt$;"<BR>":                          ' "
タグを付加して書き出す" WEND
 極めてシンプルなプログラムですので、高速に動作しそうなものです。しかし、3種類の中でも特に遅いプログラムです。どこが遅いのか結論から言うとINPUT#,PRINT#命令双方とも低速なのです。手軽な反面処理速度が犠牲になります。速度を気にしない場合は構いません。しかし、処理するデータが10万個、100万個となった場合、かなり待たされる事になります。とりあえず、ボトルネック部分であるINPUT#,PRINT#の部分の処理をうまく代替えしましょう。



高速 [inputX](メモリに読み込む)
 今度は毎時ファイルから読み込むのではなく一気にメモリに読み込んで処理を行う事にします。ファイルから読み込むよりもメモリから読み込む方が数百〜数千倍も高速です。メモリに読み込むため、メモリを確保しておく必要があります。メモリの確保の方法については、以前解説しましたので省略します。
 書き込みを行う場合も、なるべくまとめて処理しています。とりあえず1つのデータごとにファイルに書き出しをしています。
 これでINPUT#,PRINT#命令を使ったものと比べて2〜3倍高速になりました。これで十分な場合もありますが、大量にデータを処理する場合は、やはり時間がかかってしまう事があります。さらに一工夫して高速化を図ります。以下の部分が高速版のメイン処理部分です。

  WHILE adrs& < endAdrs&
    v = PEEK(adrs&):                              ' "1バイト読み込み"
    POKE wrtAdrs& + count%,v:                     ' "1文字書き込む"
    LONG IF ((v = &H0D) OR (v = &H0A) OR(v = ASC(",")))
      IF (v = &H0D) THEN INC(adrs&):              '"アドレスを1つ進める。Winの改行コードは0AH,0DHのため"
      IF count% > 0 THEN WRITE FILE #1,@wrt(0),count%
      PRINT #1,"<BR>"
      count% = 0:                                 '"書き込むバイト数をクリア"
    XELSE
      INC(count%):                                ' "書き込むバイト数を1つ増やす"
    END IF
    INC(adrs&):                                   ' "アドレスを1つ進める"
  WEND



激速版 [inputW](まとめ読み書き)
 高速版ではファイルへの書き込みはデータ1つ毎に行っていました。今度は書き込みは全てのデータができあがったら一気に書き出す事にします。さらに都合の良いことに(?)この方法だと文字数に制限はありません。INPUT#命令のような255文字までという制限はありません。メモリが許す限りOKです。
 実際に計測してみるとINPUT#,PRINT#と比べて20〜70倍、高速版と比較しても10〜30倍ほど高速になっています。68040/25MHzマシンでも約2秒です。つまり1万個のデータを処理するのに1秒、10万個でも10秒程度で処理できる事になります。これだけ速ければ十分ではないでしょうか。

  WHILE adrs& < endAdrs&
    v = PEEK(adrs&):                              ' "1バイト読み込み"
    POKE wrtAdrs&,v:                              ' "1文字書き込む"
    LONG IF ((v = &H0D) OR (v = &H0A) OR (v = ASC(",")))
      IF (v = &H0D) THEN INC(adrs&):              '"アドレスを1つ進める。Winの改行コードは0AH,0DHのため"
      POKE wrtAdrs&,&H3c:                         ' "<の数値"
      POKE wrtAdrs&+1,&H42:                       ' "Bの数値"
      POKE wrtAdrs&+2,&H52:                       ' "Rの数値"
      POKE wrtAdrs&+3,&H3e:                       ' ">の数値"
      POKE wrtAdrs&+4,&H0d:                       ' "改行コード"
      wrtAdrs& = wrtAdrs& + 5:                    ' "5バイト進める"
    XELSE
      INC(wrtAdrs&):                              ' "書き込みアドレスを増やす"
    END IF
    INC(adrs&):                                   ' "アドレスを1つ進める"
  WEND
  WRITE FILE #1,[saveHandle&],wrtAdrs&-[saveHandle&]:' "書き込んだバイト数だけファイルに書き込む"



終わりに
 ボトルネックを、どう解消するかはプログラマの腕の見せ所です。おおよそループ内での処理速度を向上させればボトルネックは解消されます。ループ外で高速化しても、ほとんど意味がないでしょう。Future BASICでボトルネックとなる部分は

 といった所でしょうか。

 今回のプログラムはダウンロードページに用意してありますが、3回目のCSVファイル選択のみ!w95(CR).csvを選択してください。これはINPUT#命令が&0AHで終わるデータではエラーを起こしてしまうためです。

 次回は・・・予告しても意味がないような気がするので予告はなしにしておきましょう(^^;




今回のプログラムリスト
sTime& = 0 eTime& = 0 END GLOBALS LOCAL FN inputX DIM wrt%(1024): '"ファイル書き出し用バッファ" openfile$ = FILES$(_fOpen,"TEXT",,openRefNum%) ' "ファイル選択ダイアログの表示" IF openfile$ = "" THEN EXIT FN: ' "キャンセルボタンが押された" savefile$ = FILES$(_fSave,"ファイル名","",saveRefNum%)' "ファイル保存ダイアログの表示" IF savefile$ = "" THEN EXIT FN: ' "キャンセルボタンが押された" DEF OPEN "TEXTttxt" ' "TeachText/SimpleText形式で保存するよう設定" OPEN "I",#1,openfile$,,openRefNum% ' "変換元のファイルをオープン" fileSize& = LOF(1,1) ' "ファイルサイズを求める" myHandle& = FN NEWHANDLE(fileSize&) ' "ファイルサイズ分メモリを確保します!" IF myHandle& = 0 THEN CLOSE #1:EXIT FN: ' "メモリ不足の場合はファイルクローズ後、関数脱出" err% = FN HLOCK(myHandle&) ' "メモリが移動しないようにハンドルをロック" READ FILE #1,[myHandle&],fileSize& ' "変換元ファイルをメモリに読み込む" CLOSE #1: ' "用済みなのでクローズする" adrs& = [myHandle&]: ' "ポインタ読み出し" endAdrs& = adrs& + fileSize&: '"最終アドレスは先頭アドレス+ファイルサイズ" OPEN "O",#1,savefile$,,saveRefNum% ' "変換先のファイルをオープン" wrtAdrs& = @wrt(0): ' "配列の先頭アドレスを設定" count% = 0: ' "書き込むバイト数を設定" sTime& = FN TICKCOUNT ' "ここからが本番f(^^?" WHILE adrs& < endAdrs& v = PEEK(adrs&): ' "1バイト読み込み" POKE wrtAdrs& + count%,v: ' "1文字書き込む" LONG IF ((v = &H0D) OR (v = &H0A) OR(v = ASC(","))) IF (v = &H0D) THEN INC(adrs&): '"アドレスを1つ進める。Winの改行コードは0AH,0DHのため" IF count% > 0 THEN WRITE FILE #1,@wrt(0),count% PRINT #1,"<BR>" count% = 0: '"書き込むバイト数をクリア" XELSE INC(count%): ' "書き込むバイト数を1つ増やす" END IF INC(adrs&): ' "アドレスを1つ進める" WEND DEF DISPOSEH(myHandle&) ' "確保したメモリを解放する" CLOSE #1: ' "ファイルクローズ" END FN ' ' "メモリを大量消費する高速版(?)" ' LOCAL FN inputW openfile$ = FILES$(_fOpen,"TEXT",,openRefNum%) ' "ファイル選択ダイアログの表示" IF openfile$ = "" THEN EXIT FN: ' "キャンセルボタンが押された" savefile$ = FILES$(_fSave,"ファイル名","",saveRefNum%)' "ファイル保存ダイアログの表示" IF savefile$ = "" THEN EXIT FN: ' "キャンセルボタンが押された" DEF OPEN "TEXTttxt" ' "TeachText/SimpleText形式で保存するよう設定" OPEN "I",#1,openfile$,,openRefNum% ' "変換元のファイルをオープン" fileSize& = LOF(1,1) ' "ファイルサイズを求める" myHandle& = FN NEWHANDLE(fileSize&) ' "ファイルサイズ分メモリを確保します!" IF myHandle& = 0 THEN CLOSE #1:EXIT FN: ' "メモリ不足の場合はファイルクローズ後、関数脱出" saveHandle& = FN NEWHANDLE(fileSize&*2) ' "ファイルサイズ分メモリを確保します!" IF saveHandle& = 0 THEN CLOSE #1:DEF DISPOSEH(myHandle&):EXIT FN:' "メモリ不足の場合はファイルクローズ、メモリ解放後、関数脱出" err% = FN HLOCK(myHandle&) ' "メモリが移動しないようにハンドルをロック" err% = FN HLOCK(saveHandle&) ' "メモリが移動しないようにハンドルをロック" READ FILE #1,[myHandle&],fileSize& ' "変換元ファイルをメモリに読み込む" CLOSE #1: ' "用済みなのでクローズする" adrs& = [myHandle&]: ' "ポインタ読み出し" endAdrs& = adrs& + fileSize&: '"最終アドレスは先頭アドレス+ファイルサイズ" OPEN "O",#1,savefile$,,saveRefNum% ' "変換先のファイルをオープン" wrtAdrs& = [saveHandle&]: ' "格納先の先頭アドレスを設定" sTime& = FN TICKCOUNT ' "ここからが本番f(^^?" WHILE adrs& < endAdrs& v = PEEK(adrs&): ' "1バイト読み込み" POKE wrtAdrs&,v: ' "1文字書き込む" LONG IF ((v = &H0D) OR (v = &H0A) OR (v = ASC(","))) IF (v = &H0D) THEN INC(adrs&): '"アドレスを1つ進める。Winの改行コードは0AH,0DHのため" POKE wrtAdrs&,&H3c: ' "<の数値" POKE wrtAdrs&+1,&H42: ' "Bの数値" POKE wrtAdrs&+2,&H52: ' "Rの数値" POKE wrtAdrs&+3,&H3e: ' ">の数値" POKE wrtAdrs&+4,&H0d: ' "改行コード" wrtAdrs& = wrtAdrs& + 5: ' "5バイト進める" XELSE INC(wrtAdrs&): ' "書き込みアドレスを増やす" END IF INC(adrs&): ' "アドレスを1つ進める" WEND WRITE FILE #1,[saveHandle&],wrtAdrs&-[saveHandle&]:' "書き込んだバイト数だけファイルに書き込む" DEF DISPOSEH(myHandle&) ' "確保したメモリを解放する" CLOSE #1: ' "ファイルクローズ" END FN LOCAL FN inputN openfile$ = FILES$(_fOpen,"TEXT",,openRefNum%) ' "ファイル選択ダイアログの表示" IF openfile$ = "" THEN EXIT FN: ' "キャンセルボタンが押された" savefile$ = FILES$(_fSave,"ファイル名","",saveRefNum%)' "ファイル保存ダイアログの表示" IF savefile$ = "" THEN EXIT FN: ' "キャンセルボタンが押された" DEF OPEN "TEXTttxt" ' "TeachText/SimpleText形式で保存するよう設定" OPEN "I",#1,openfile$,,openRefNum% ' "変換元のファイルをオープン" OPEN "O",#2,savefile$,,saveRefNum% ' "変換先のファイルをオープン" sTime& = FN TICKCOUNT WHILE NOT EOF(1) INPUT #1,dt$: ' "カンマまたは改行まで読み込む" PRINT #2,dt$;"<BR>": ' "<BR>タグを付加して書き出す" WEND CLOSE #1,#2 END FN DEFSTR LONG WINDOW OFF WINDOW #1,"結 果",(0,0)-(400,270),_doc TEXT _applFont,12 FN inputW eTime& = FN TICKCOUNT PRINT "High Speed Program (Mem)" PRINT "Start Time:";sTime& PRINT "End Time:";eTime& PRINT "-----------------" PRINT "Result :";eTime& - sTime& PRINT STOP FN inputX eTime& = FN TICKCOUNT PRINT "High Speed Program" PRINT "Start Time:";sTime& PRINT "End Time:";eTime& PRINT "-----------------" PRINT "Result :";eTime& - sTime& PRINT FN inputN eTime& = FN TICKCOUNT PRINT "Normal Program" PRINT "Start Time:";sTime& PRINT "End Time:";eTime& PRINT "-----------------" PRINT "Result :";eTime& - sTime& STOP