READ系関数のEOF-ERROR-Pを活用できないか — #:g1

Posted 2010-12-07 14:15:00 GMT

READ系関数のEOF-ERROR-PはデフォルトがTですが、普通使うときは、大抵NILにすることが多いと思います。
これのデフォルトがNILだったら、

(loop :for line := (read-line in nil) :while line :collect line)
ではなく、
(loop :for line := (read-line in) :while line :collect line)
のようにすっきり書けたりするのになと常々思っていますが、逆に、これを活用できないかということで、以前、積極的にEND-OF-FILEシグナルを拾いに行く試みをしたことがありました。
-(http://g000001.cddddr.org/1263305868)
今日CLerのquekさんとの雑談の中で、もしかしたらループの中でEOFかどうかを比較するコストが低いかもしれず、結果として速いかもしれない、という話になり折角なので実験してみることにしました。
まずは、/usr/share/dict/wordsでの比較。98569行のファイルです。
;; EOFをつかまえる
(dotimes (i 100)
  (with-open-file (in "/usr/share/dict/words")
    (atap 0
      (handler-case (loop :for c := (read-char in) :do (incf it))
        (end-of-file () nil)))))
;⇒ NIL
----------
Evaluation took:
  7.640 seconds of real time
  7.620000 seconds of total run time (7.580000 user, 0.040000 system)
  99.74% CPU
  18,289,984,221 processor cycles
  1,200,560 bytes consed

Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz

;; atap (atap 0 ...) ≡ (LET ((IT 0)) ... IT)

;; 通常
(dotimes (i 100)
  (with-open-file (in "/usr/share/dict/words")
    (loop :for c := (read-char in nil nil) :while c :count c)))
;⇒ NIL
----------
Evaluation took:
  7.996 seconds of real time
  7.990000 seconds of total run time (7.900000 user, 0.090000 system)
  99.92% CPU
  19,144,510,308 processor cycles
  1,102,048 bytes consed

Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz

何度か平均してみましたが、極わずかのほんのちょっとだけ速い気がします。

次に512MBのファイルの読み込みを試してみます
$ dd if=/dev/zero of=/tmp/test.img bs=1024 count=$((1024*512))
;; 通常 ループ内では、-1と比較
(with-open-file (in "/share/music/tmp/test.img" :element-type '(unsigned-byte 8))
  (loop :for b := (read-byte in nil -1) :until (= -1 b) :count b))
;⇒ 536870912
----------
Evaluation took:
  35.383 seconds of real time
  34.980000 seconds of total run time (34.690000 user, 0.290000 system)
  98.86% CPU
  84,706,093,341 processor cycles
  512,976 bytes consed

Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz

;; EOFを掴まえる
(with-open-file (in "/share/music/tmp/test.img" :element-type '(unsigned-byte 8))
  (atap 0
    (handler-case (loop :for c := (read-byte in) :do (incf it))
      (end-of-file () nil))))
;⇒ 536870912
----------
Evaluation took:
  34.367 seconds of real time
  33.790000 seconds of total run time (33.520000 user, 0.270000 system)
  98.32% CPU
  82,272,802,536 processor cycles
  506,608 bytes consed

Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz

これもほんのちょっ〜とだけ速い気がします。
おまけだと、IGNORE-ERRORSで捕捉しても速いことは速いです。スタイル的に微妙ですが…。
(with-open-file (in "/share/music/tmp/test.img" :element-type '(unsigned-byte 8))
  (ignore-errors (loop (read-byte in))))
;⇒ NIL
----------
Evaluation took:
  32.416 seconds of real time
  32.330000 seconds of total run time (32.040000 user, 0.290000 system)
  99.73% CPU
  77,605,457,589 processor cycles
  478,112 bytes consed

Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz


結論としては、微妙〜に速い気がする程度という感じになりましたが、探してみれば良い使いどころがあったりすることに期待したいです。逆に使ったら絶対駄目なところがあるのかもしれませんが。

おまけ HANDLER-CASEでEND-OF-FILEを掴まえるスタイルの欠点

HANDLER-CASEでEND-OF-FILEを掴まえるスタイルの欠点ですが、必ずエラーで抜けるため、フォームの返り値が使えないという欠点があります。
周りを値を収集するフォームで囲ってやらないと使い勝手が悪くなりますが、ここで、SeriesのGATHERERを使うと綺麗に書けます。
(with-open-file (in "/etc/passwd")
  (gatherlet ((g collect))
    (handler-case (loop (next-out g (read-line in)))
      (end-of-file () nil))
    (result-of g)))

;; gatherer (with-open-file (in "/etc/passwd") (gathering ((g collect)) (handler-case (loop (next-out g (read-line in))) (end-of-file () nil))))

まあ、Seriesには、SCAN-FILEがありますが。

comments powered by Disqus