7

これは、一種のフォローアップの質問です:効率的な文字列置換関数を書きますか? .

(遠い将来ではありますが) 将来的には自然言語処理をやりたいと思っています。もちろん、文字列操作の速度は重要です。偶然にも、私はこのテストに出くわしました: http://raid6.com.au/~onlyjob/posts/arena/ - すべてのテストには偏りがあります。これも例外ではありません。しかし、それは私にとって重要な問題を提起しました。それで、私は自分がどのようにやっているのかを確認するためにいくつかのテストを書きました:

これは私の最初の試みでした (#A と呼びます):

#A

(defun test ()
  (declare (optimize (debug 0) (safety 0) (speed 3)))
  (loop with addidtion = (concatenate 'string "abcdefgh" "efghefgh")
     and initial = (get-internal-real-time)
     for i from 0 below (+ (* (/ 1024 (length addidtion)) 1024 4) 1000)
     for ln = (* (length addidtion) i)
     for accumulated = addidtion
     then (loop with concatenated = (concatenate 'string accumulated addidtion)
             for start = (search "efgh" concatenated)
             while start do (replace concatenated "____" :start1 start)
             finally (return concatenated))
     when (zerop (mod ln (* 1024 256))) do
       (format t "~&~f s | ~d Kb" (/ (- (get-internal-real-time) initial) 1000) (/ ln 1024)))
  (values))

(test)

結果に戸惑い、使用しようとしましたcl-ppcre-何を望んでいるのかわかりませんが、結果は本当に悪いものでした...テストに使用したコードは次のとおりです。

#B

(ql:quickload "cl-ppcre")

(defun test ()
  (declare (optimize (debug 0) (safety 0) (speed 3)))
  (loop with addidtion = (concatenate 'string "abcdefgh" "efghefgh")
     and initial = (get-internal-real-time)
     for i from 0 below (+ (* (/ 1024 (length addidtion)) 1024 4) 1000)
     for ln = (* (length addidtion) i)
     for accumulated = addidtion
     then (cl-ppcre:regex-replace-all "efgh" (concatenate 'string accumulated addidtion) "____")
     when (zerop (mod ln (* 1024 256))) do
       (format t "~&~f s | ~d Kb" (/ (- (get-internal-real-time) initial) 1000) (/ ln 1024)))
  (values))

(test)

さて、おそらくいくつかの一般化を回避することを期待して、やや単純なバージョンではありますが、独自のバージョンを作成することにしました。

#C

(defun replace-all (input match replacement)
  (declare (type string input match replacement)
           (optimize (debug 0) (safety 0) (speed 3)))
  (loop with pattern fixnum = (1- (length match))
     with i fixnum = pattern
     with j fixnum = i
     with len fixnum = (length input) do
       (cond
         ((>= i len) (return input))
         ((zerop j)
          (loop do
               (setf (aref input i) (aref replacement j) i (1+ i))
               (if (= j pattern)
                   (progn (incf i pattern) (return))
                   (incf j))))
         ((char= (aref input i) (aref match j))
          (decf i) (decf j))
         (t (setf i (+ i 1 (- pattern j)) j pattern)))))

(defun test ()
  (declare (optimize (debug 0) (safety 0) (speed 3)))
  (loop with addidtion string = (concatenate 'string "abcdefgh" "efghefgh")
     and initial = (get-internal-real-time)
     for i fixnum from 0 below (+ (* (/ 1024 (length addidtion)) 1024 4) 1000)
     for ln fixnum = (* (length addidtion) i)
     for accumulated string = addidtion
     then (replace-all (concatenate 'string accumulated addidtion) "efgh" "____")
     when (zerop (mod ln (* 1024 256))) do
       (format t "~&~f s | ~d Kb" (/ (- (get-internal-real-time) initial) 1000) (/ ln 1024)))
  (values))

(test)

とほぼ同じくらい遅いcl-ppcre!今、それは信じられないほどです!このようなパフォーマンスの低下をもたらすようなものをここで見つけることができるものはありません...それでも、それはひどいです:(

標準関数がこれまでのところ最高のパフォーマンスを発揮していることに気づき、SBCL ソースを調べ、いくつか読んだ後、次のことを思いつきました。

#D

(defun replace-all (input match replacement &key (start 0))
  (declare (type simple-string input match replacement)
           (type fixnum start)
           (optimize (debug 0) (safety 0) (speed 3)))
  (loop with input-length fixnum = (length input)
     and match-length fixnum = (length match)
     for i fixnum from 0 below (ceiling (the fixnum (- input-length start)) match-length) do
       (loop with prefix fixnum = (+ start (the fixnum (* i match-length)))
          for j fixnum from 0 below match-length do
            (when (<= (the fixnum (+ prefix j match-length)) input-length)
              (loop for k fixnum from (+ prefix j) below (the fixnum (+ prefix j match-length))
                 for n fixnum from 0 do
                   (unless (char= (aref input k) (aref match n)) (return))
                 finally
                   (loop for m fixnum from (- k match-length) below k
                      for o fixnum from 0 do
                        (setf (aref input m) (aref replacement o))
                      finally
                        (return-from replace-all
                          (replace-all input match replacement :start k))))))
       finally (return input)))

(defun test ()
  (declare (optimize (debug 0) (safety 0) (speed 3)))
  (loop with addidtion string = (concatenate 'string "abcdefgh" "efghefgh")
     and initial = (get-internal-real-time)
     for i fixnum from 0 below (+ (* (/ 1024 (length addidtion)) 1024 4) 1000)
     for ln fixnum = (* (length addidtion) i)
     for accumulated string = addidtion
     then (replace-all (concatenate 'string accumulated addidtion) "efgh" "____")
     when (zerop (mod ln (* 1024 256))) do
       (format t "~&~f s | ~d Kb" (/ (- (get-internal-real-time) initial) 1000) (/ ln 1024)))
  (values))

(test)

最後に、標準ライブラリに対するパフォーマンスはごくわずかですが、勝つことができます-それでも、他のほとんどすべてに比べて非常に悪いです...

結果の表は次のとおりです。

| SBCL #A   | SBCL #B   | SBCL #C    | SBCL #D   | C gcc 4 -O3 | String size |
|-----------+-----------+------------+-----------+-------------+-------------|
| 17.463 s  | 166.254 s | 28.924 s   | 16.46 s   | 1 s         | 256 Kb      |
| 68.484 s  | 674.629 s | 116.55 s   | 63.318 s  | 4 s         | 512 Kb      |
| 153.99 s  | gave up   | 264.927 s  | 141.04 s  | 10 s        | 768 Kb      |
| 275.204 s | . . . . . | 474.151 s  | 251.315 s | 17 s        | 1024 Kb     |
| 431.768 s | . . . . . | 745.737 s  | 391.534 s | 27 s        | 1280 Kb     |
| 624.559 s | . . . . . | 1079.903 s | 567.827 s | 38 s        | 1536 Kb     |

さて、質問です:私は何を間違えましたか?これは Lisp 文字列に固有のものですか? これはおそらく...何によって軽減できますか?

長い目で見れば、文字列処理専用のライブラリを作成することも検討したいと思います。問題が私の悪いコードではなく、実装である場合。そうするのは理にかなっていますか?はいの場合、それを行うためにどの言語をお勧めしますか?


編集:記録のために、私は今このライブラリを使用しようとしています: https://github.com/Ramarren/ropes文字列の連結を処理します。残念ながら、これには置換機能がなく、複数の置換を行うのは簡単ではありません。でも何かあったらこの記事は更新します。


文字列連結の代わりに配列のフィルポインターを使用するように huaiyuan のバリアントを少し変更しようとしました ( StringBuilderPaulo Madeira によって提案されたのと同様のことを達成するためです。おそらくさらに最適化することができますが、メソッドになる型 / についてはわかりませんより速くなる / の型を再定義して、 orでのみ動作するようにする価値は*ありますか. とにかく、コードとベンチマークは次のとおりです。+fixnumsigned-byte

(defun test/e ()
  (declare (optimize speed))
  (labels ((min-power-of-two (num)
             (declare (type fixnum num))
             (decf num)
             (1+
              (progn
                (loop for i fixnum = 1 then (the (unsigned-byte 32) (ash i 1))
                   while (< i 17) do
                     (setf num
                           (logior
                            (the fixnum
                                 (ash num (the (signed-byte 32)
                                               (+ 1 (the (signed-byte 32)
                                                         (lognot i)))))) num))) num)))
           (join (x y)
             (let ((capacity (array-dimension x 0))
                   (desired-length (+ (length x) (length y)))
                   (x-copy x))
               (declare (type fixnum capacity desired-length)
                        (type (vector character) x y x-copy))
               (when (< capacity desired-length)
                 (setf x (make-array
                          (min-power-of-two desired-length)
                          :element-type 'character
                          :fill-pointer desired-length))
                 (replace x x-copy))
               (replace x y :start1 (length x))
               (setf (fill-pointer x) desired-length) x))
           (seek (old str pos)
             (let ((q (position (aref old 0) str :start pos)))
               (and q (search old str :start2 q))))
           (subs (str old new)
             (loop for p = (seek old str 0) then (seek old str p)
                while p do (replace str new :start1 p))
             str))
    (declare (inline min-power-of-two join seek subs)
             (ftype (function (fixnum) fixnum) min-power-of-two))
    (let* ((builder
            (make-array 16 :element-type 'character
                        :initial-contents "abcdefghefghefgh"
                        :fill-pointer 16))
           (ini (get-internal-real-time)))
      (declare (type (vector character) builder))
      (loop for i fixnum below (+ 1000 (* 4 1024 1024 (/ (length builder))))
         for j = builder then
           (subs (join j builder) "efgh" "____")
         for k fixnum = (* (length builder) i)
         when (= 0 (mod k (* 1024 256)))
         do (format t "~&~8,2F sec ~8D kB"
                    (/ (- (get-internal-real-time) ini) 1000)
                    (/ k 1024))))))

    1.68 sec      256 kB
    6.63 sec      512 kB
   14.84 sec      768 kB
   26.35 sec     1024 kB
   41.01 sec     1280 kB
   59.55 sec     1536 kB
   82.85 sec     1792 kB
  110.03 sec     2048 kB
4

2 に答える 2

5

ボトルネックはsearchおそらく SBCL で最適化されていない関数です。次のバージョンはposition、不可能な領域をスキップするのに役立ち、私のマシンのバージョン #A の約 10 倍高速です。

(defun test/e ()
  (declare (optimize speed))
  (labels ((join (x y)
             (concatenate 'simple-base-string x y))
           (seek (old str pos)
             (let ((q (position (char old 0) str :start pos)))
               (and q (search old str :start2 q))))
           (subs (str old new)
             (loop for p = (seek old str 0) then (seek old str p)
                   while p do (replace str new :start1 p))
             str))
    (declare (inline join seek subs))
    (let* ((str (join "abcdefgh" "efghefgh"))
           (ini (get-internal-real-time)))
      (loop for i below (+ 1000 (* 4 1024 1024 (/ (length str))))
            for j = str then (subs (join j str) "efgh" "____")
            for k = (* (length str) i)
            when (= 0 (mod k (* 1024 256)))
              do (format t "~&~8,2F sec ~8D kB"
                         (/ (- (get-internal-real-time) ini) 1000)
                         (/ k 1024))))))
于 2013-07-14T17:10:21.467 に答える
2

そのページのテストは確かに偏っているので、どれだけ見てみましょう. 著者は文字列操作をテストすると主張していますが、そのページのプログラムがテストしているものは次のとおりです。

  • 文字列連結
  • 明示的 (C) または暗黙的なメモリ管理
  • 一部の言語では、正規表現
  • 他では、文字列検索アルゴリズムと部分文字列置換
    • 複数の言語で境界チェックを行うメモリーアクセス

ここにはあまりにも多くの側面があります。測定方法は次のとおりです。

  • 秒単位のリアルタイム

サービス、ウイルス対策、ブラウザ、待機中の *nix シェルなど、他のプロセスをまったく使用せずに、妥当な値を取得するためにこのテストだけを実行することにコンピューターを完全に専念させる必要があったため、これは残念なことです。CPU 時間の方がはるかに便利で、仮想マシンでテストを実行することもできます。

もう 1 つの側面は、C、C++、Perl、Python、PHP、Ruby の文字は 8 ビットですが、他の多くのテスト済み言語では 16 ビットです。これは、メモリ使用量が大きく異なる量 (少なくとも 2 倍) でストレスを受けることを意味します。ここでは、キャッシュ ミスがはるかに顕著になります。

Perl が非常に高速な理由は、境界を常にチェックするのではなく、C 関数を呼び出す前に引数を 1 回チェックするためだと思います。8 ビット文字列を使用する他の言語はそれほど高速ではありませんが、それでも十分高速です。

JavaScript V8 には、可能であれば ASCII の文字列が含まれているため、追加および置換されたトークンが「ëfgh」であった場合、その実装ではより多くの費用がかかります。

Python 3 は Python 2 よりもほぼ 3 倍遅く、私の推測では、文字列のwchar_t *vschar *内部表現によるものです。

JavaScript SpiderMonkey は 16 ビット文字列を使用します。私はソースをあまり掘り下げませんでしたが、ファイル jsstr.h はロープについて言及しています。

s は不変であるため、Java は非常に低速ですString。したがって、このベンチマークでは、適切なデータ型ではないことは明らかです。それぞれの後に巨大な文字列を生成する代償を払っています.replace()。私はテストしていませんが、おそらくaのStringBuffer方がはるかに高速です。

したがって、このベンチマークは、一粒の塩だけでなく、一握りの塩で取得する必要があります.


arefCommon Lisp では、境界チェックと型ディスパッチsetfがおそらくボトルネックです。

良好なパフォーマンスを得るには、一般的なstringシーケンスを捨てて、 simple-strings またはsimple-vectors のうち、実装が最も最適化する方を使用する必要があります。次に、境界チェックをバイパスするscharorsvrefおよびそのableフォームを呼び出す方法が必要です。setfここから、独自のsimple-string-searchor simple-character-vector-search(およびreplace-simple-stringor replace-simple-vector、この特定の例でははるかに小さい役割を果たしますが) を実装して、配列アクセスごとではなく各呼び出しの先頭で境界チェックを行い、最高速度の最適化と型宣言を行うことができます。

十分にスマートなコンパイラ™は、「適切な」宣言が与えられれば、これらすべてを実行します。問題は、(concatenate 'simple-string/simple-vector ...)単純な文字列も単純なベクトルも調整できないため、 を使用する必要があることです。

圧縮/移動 GC では、これらの場合 (配列/オブジェクトのコピーなど) に常にペナルティがあり、配列の調整と連結のどちらを選択するかは、プロファイリング テストに依存する必要があります。そうしないと、配列を大きくするのに十分な空きメモリがあるにもかかわらず、調整が連結よりもはるかに高速になる可能性があります。

実装が調整可能な配列の最適化された呼び出し/拡張の先頭で簡単な境界チェックの直後に実際の要素にアクセスする場合は、調整可能な配列を使用できsearchますreplace(たとえば、実際の変位ベクトル/配列を取得して開始する内部定義を持つことにより)エンドオフセット)。

しかし、私はここで多くのことを推測しています.実際の事実については、コンパイルし、コンパイルを検査し、各実装でプロファイルを作成する必要があります.


余談ですが、C のサンプル コードには、オフ バイ ワン (実際には -1) の割り当て (strcat呼び出しが余分なバイト、ゼロで終わる文字列ターミネータを書き込む)、初期化されていないゼロで終わる文字列など、バグがたくさんあります。 (メモリが 0 に初期化されない可能性があるため、gstr最初は運によって機能します)、書式文字列でのこれらの型の変換と仮定、考慮せずにインクリメントされる最初の割り当てで初期化される未使用の変数バッファを移動する可能性があり、エラー処理はまったくありません。strcatsize_ttime_tintprintfpos_cgstrrealloc

于 2013-07-18T19:11:16.147 に答える