【问题标题】:Mandelbrot set implementation in Scheme is very slowScheme中的Mandelbrot集实现非常慢
【发布时间】:2015-11-02 16:22:52
【问题描述】:

我正在尝试学习 Lisp/Scheme,并尝试在其中实现一个非常简单的 mandelbrot 版本以进行练习。我遇到的问题是代码运行非常非常慢。起初我以为是因为我使用递归而不是命令式循环,但我尝试在 python 中重写或多或少相同的代码(包括递归)(甚至没有尾调用优化),它运行很流畅

所以我想知道我的代码中是否缺少一些明显的东西,以及我可以做些什么来让它运行得更快。

这是 Scheme (racket) 中的代码 sn-p。我在 SBCL 中也做了几乎相同的事情,而且速度相当

#lang racket

(define-syntax dotimes 
   (syntax-rules () 
     ((_ (var n res) . body) 
      (do ((limit n) 
           (var 0 (+ var 1))) 
          ((>= var limit) res) 
        . body)) 
     ((_ (var n) . body) 
      (do ((limit n) 
           (var 0 (+ var 1))) 
          ((>= var limit)) 
        . body))))

(define (print-brot zr zc)
  (if (< (+ (* zr zr) (* zc zc)) 2)
      (display "@")
      (display ".")))

(define (brot zr zc cr cc i)
  (if (= i 0)
      (print-brot zr zc)
      (let ((z2r (- (* zr zr) (* zc zc))) (z2c (* 2 zr zc)))
        (brot (+ z2r cr) (+ z2c cc) cr cc (- i 1)))))

(define (linspace i w)
  (/ (- i (/ w 2)) (/ w 4)))

(define (brot-grid w h n)
  (dotimes (i w)
           (dotimes (j h)
                    (let ((x (linspace i w)) (y (linspace j h)))
                      (brot 0 0 x y n)))
           (newline)))

(brot-grid 40 80 20)

(我希望代码块不要太密集,很难把它剥离成更简单的东西)

另外,我知道 Scheme 和 Common Lisp 内置了复数,但我想使用常规实数对其进行测试,我认为这不是它运行如此缓慢的原因。

brot函数的参数“i”是迭代次数,brot-grid的参数“n”也是每个点使用的迭代次数。当我将它设置为超过 10 时,代码需要永远运行,这似乎不正常。所用时间的增加似乎也不是线性的,例如在我的机器上 n = 10 只需要大约一秒钟,但在 n = 15 时需要几分钟,甚至在 n = 20 时都没有完成

那么,是什么让这段代码运行如此缓慢?

提前致谢

【问题讨论】:

  • zrzc 应该很大吗?我暂停了一下,zr 有超过 4000 个数字。由于 Scheme 有 bigintegers,它不会抱怨大小,直到所有程序内存都被消耗完。
  • '实数'?浮动?如果您想使用实数/浮点数进行计算,那么您应该确保您实际使用它们并且您的操作也使用它们。我看到很多整数和有理运算,它们可能会很慢,例如在使用大数或大有理数时。只需跟踪或步进函数,您就会看到函数使用的数字。
  • 超过 10 次迭代? Pshaw,想象一下1000!即使您可以实现非常快速的迭代方式,生成 Mset 也会很慢。如果您只是以基本方式编写代码,它会非常慢。它是计算密集型的,所以也许你需要更好的语言选择。而且,您不需要复数函数,只需要非常快速的方法来处理定点整数或 FPU 实数。
  • 谢谢,问题似乎确实与数字的表示有关。当 Z 的模块大于 2 时将其更改为停止迭代已经使它更快。确保使用正确的数字表示应该照顾其余部分。我想我不习惯方案似乎会根据您对它们所做的操作神奇地改变数字的表示方式
  • 所以基本上只需将linspace 中的常量2 更改为2.0 即可使整个事情在十分之一秒内完成。 @EtienneBoulais 这是一个错误。它只是对没有平方根的平方求和,所以该值确实应该是 4

标签: recursion scheme lisp common-lisp mandelbrot


【解决方案1】:

查看您的代码,我认为您正在使用有理数对其进行测试。这意味着相当精确的算术运算,缺点是您很快就会使用带有巨大大数作为分子和分母的有理数。

确保您使用浮点数(我建议使用双浮点数)的一种方法是拥有一个将所有输入转换为双精度数的中间函数,以便更容易输入(比如说)0 而不是 @ 987654322@.

一旦您确定使用双精度可以使其更快,您就可以开始在整个过程中添加类型声明,以使编译器能够为您生成更好的代码。

【讨论】:

  • 你可以将你的理性限制在任何精度,例如小数点后 100 位。这将比 doubles 慢,但比精确的有理数要快得多。
  • 我的意思是,简单地乘以 (10^n),截断到最接近的整数,然后用 10^n 分母形成一个新的比率。这不是正确的事情,但它会做,如果不是,增加 n 一些。 :)
  • @WillNess 那行得通,但在这一点上,简单地使用双浮点可能更容易(而且它可能更精确地接近 0.0d0 无论如何......)。 :)
  • 好吧,在这个方案中提取尾数应该很容易,就像双打一样。那么,100(或 1000)个有意义的数字。 :) 当然,这是一个真正粗略的“解决方案”(因为它无法控制累积错误)。我敢肯定,有真正的自适应精度方案。
【解决方案2】:

这是一个 Common Lisp 变体:

(defun print-brot (zr zc)
  (write-char (if (< (+ (* zr zr)
                        (* zc zc))
                     2.0d0)
                  #\@
                #\.)))

(defun brot (zr zc cr cc i)
  (loop repeat i
        for z2r = (- (* zr zr) (* zc zc))
        for z2c = (* 2.0d0 zr zc)
        until (or (> (abs zr) 1.0d50)
                  (> (abs zc) 1.0d50))
        do (setf zr (+ z2r cr)
                 zc (+ z2c cc)))
  (print-brot zr zc))

(defun linspace (i w)
  (/ (- i (/ w 2.0d0)) (/ w 4.0d0)))

(defun brot-grid (w h n)
  (terpri)
  (loop for i from 0.0d0 by 1.0d0
        repeat w do
        (loop for j from 0.0d0 by 1.0d0
              repeat h do
              (brot 0.0d0 0.0d0 (linspace i w) (linspace j h) n))
    (terpri)))

注意双浮点常量的使用。还对双浮点数和整数进行迭代。

在未优化的 SBCL 中运行它,但已编译为本机代码:

*  (time (brot-grid 20 40 20))

........................................
....................@...................
....................@...................
....................@...................
...................@@@..................
.................@@@@@@@................
...................@@@..................
................@@@@@@@@@...............
..............@@@@@@@@@@@@@.............
............@@@@@@@@@@@@@@@@@...........
..............@@@@@@@@@@@@@.............
...............@@@@@@@@@@@..............
..................@...@.................
........................................
........................................
........................................
........................................
........................................
........................................
........................................
Evaluation took:
  0.003 seconds of real time
  0.002577 seconds of total run time (0.001763 user, 0.000814 system)
  100.00% CPU
  6,691,716 processor cycles
  2,064,384 bytes consed

然后优化代码意味着:

  • 更高的编译器优化设置
  • 可能会添加一些类型声明
  • 摆脱浮标

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多