提高字符串操作的速度

时间:2013-07-13 11:47:46

标签: performance common-lisp

对于这个问题,这是一个后续问题:Write an efficient string replacement function?

在(虽然遥远)未来,我希望能够进行自然语言处理。当然,字符串操作的速度很重要。无意中,我偶然发现了这个测试: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的变体来使用数组的填充指针而不是字符串连接(以实现类似于Paulo Madeira建议的StringBuilder。它可能会进一步优化,但我不是确定类型/哪种方法更快/重新定义*+的类型以使它们仅在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

2 个答案:

答案 0 :(得分: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))))))

答案 1 :(得分:2)

该页面中的测试确实存在偏差,所以让我们看看它有多少。作者声称测试字符串操作,但这是该页面中的程序测试:

  • 字符串连接
  • 内存管理,显式(C)或隐式
  • 在某些语言中,正则表达式
  • 在其他情况下,字符串搜索算法和子字符串替换
    • 内存访问,已对多种语言进行检查

这里有很多方面。以下是它的测量方法:

  • 以秒为单位的实时

这是不幸的,因为计算机必须完全专注于运行这个测试以获得合理的值,而不需要任何其他过程,例如服务,防病毒,浏览器,甚至等待* nix shell。 CPU时间会更有用,您甚至可以在虚拟机中运行测试。

另一个方面是C,C ++,Perl,Python,PHP和Ruby中的字符是8位,但在许多其他测试语言中它们都是16位。这意味着内存使用量的压力差异很大,至少为2倍。这里,缓存未命中率更加明显。

我怀疑Perl之所以这么快是因为它在调用C函数之前检查了它的参数,而不是经常检查边界。其他具有8位字符串的语言并不是那么快,但仍然相当快。

如果可能的话,JavaScript V8的字符串是ASCII,所以如果附加和替换的标记是“ëfgh”,那么你需要在该实现中支付更多费用。

Python 3几乎比Python 2慢三倍,我的猜测是由于字符串的wchar_t * vs char *内部表示。

JavaScript SpiderMonkey使用16位字符串。我没有多挖源,但文件jsstr.h提到了绳索。

Java速度很慢,因为String是不可变的,因此对于这个基准测试,它绝对不是合适的数据类型。您需要为每个.replace()后生成一个巨大的字符串付出代价。我没有测试过,但是StringBuffer可能要快得多。

所以,这个基准不仅要用一粒盐,还要用少量盐。


在Common Lisp中,aref及其setf中的边界检查和类型调度可能是瓶颈。

为了获得良好的性能,您必须抛弃通用的string序列并使用simple-stringsimple-vector s,无论您的实现最佳优化。然后,您应该有一种方法可以调用scharsvref及其setf能够绕过边界检查的表单。从这里开始,您可以实施自己的simple-string-searchsimple-character-vector-search(以及replace-simple-stringreplace-simple-vector,尽管它们在此特定示例中扮演的角色要小得多),并且可以全速优化类型声明,在每个调用的头部检查边界,而不是在每个数组访问时检查。

一个足够聪明的编译器™会在给出“正确”声明的情况下为您完成所有这些工作。问题是,你必须使用(concatenate 'simple-string/simple-vector ...),因为简单字符串和简单向量都不可调。

使用压缩/移动GC,在这些情况下总是会受到惩罚(例如阵列/对象复制),并且在数组调整和连接之间进行选择必须真正依赖于性能分析测试。否则,调整可能比连接更快,而有足够的可用内存来增长数组。

你可以使用可调整的数组,如果实现将在优化调用/ searchreplace的可调数组调用(例如通过具有内部定义,采用实际位移矢量/数组以及开始和结束偏移。)

但是我在这里推测很多,你必须编译,检查每个实现中的编译和配置文件以获得真实的事实。


作为旁注,C示例代码充满了错误,例如逐个(实际上是-1)分配(strcat调用写一个额外字节,零终止字符串终止符),未初始化的以零结尾的字符串gstr(第一个strcat运气好,因为内存可能未初始化为0),从size_ttime_t转换为int并假设printf格式字符串中的这些类型,未使用的变量pos_c,使用gstr的第一个分配进行初始化,但不考虑{realloc {1}}可以移动缓冲区,并且不会进行任何错误处理。

相关问题