【问题标题】:Racket streams slower than custom streams?球拍流比自定义流慢?
【发布时间】:2019-09-30 11:48:35
【问题描述】:

我实现了一个简单但不是很有效的 Eratosthenes 筛子。一次用于内置球拍流,一次用于自定义流。对我而言,唯一已知的区别是内置流正在评估第一个待命项目,而不是在施工中。在两种实现上评估前 1000 个素数时,自定义流的运行速度提高了 10-20 倍。有什么解释吗?

(define (integers-starting-from-stream n)
  (stream-cons n (integers-starting-from-stream (+ n 1))))

(define (stream-limit s limit)
    (when (not (= limit 0)) (stream-limit (stream-rest s) (- limit 1))))

(define x (integers-starting-from-stream 2))

(define (divisible? x y)
  (= (remainder x y) 0))

(define (sieve s)
  (stream-cons
   (stream-first s)
   (sieve (stream-filter
           (lambda (x)
             (not (divisible? x (stream-first s))))
           (stream-rest s)))))
(time (stream-limit (sieve x) 1000))

或者我错过了影响性能的东西?

(define-syntax-rule (s-delay exp)
  (λ() exp))

(define (s-force delayedObject)
  (delayedObject))

(define empty-s 'S-EMPTY-STREAM)

(define (s-empty? s)
  (eq? s empty-s))

(define (s-first s)
  (car s))

(define (s-rest s)
  (s-force (cdr s)))

(define-syntax-rule (s-cons a b)
  (cons a (s-delay b)))
(define (s-filter p s)
  (cond ((s-empty? s) empty-s)
        ((p (s-first s))
         (s-cons (s-first s)
                 (s-filter p (s-rest s))))
        (else (s-filter p (s-rest s)))))
;;;;;;;;;;;;;;;;;;;;
(define (divisible? x y)
  (= (remainder x y) 0))

(define (integers-starting-from n)
  (s-cons n (integers-starting-from (+ n 1))))

(define (s-limit s limit)
    (when (not (= limit 0)) (s-limit (s-rest s) (- limit 1))))

(define x (integers-starting-from 2))

(define (sieve s)
  (s-cons (s-first s) (sieve (s-filter (lambda(x) (not (divisible? x (s-first s)))) (s-rest s)))))
(time (s-limit (sieve x) 1000))

【问题讨论】:

  • 我假设您正在运行已编译的代码。我已经玩了一点,虽然你的实现跳过了记忆和懒惰car,但如果我添加它,我只会慢 5 倍。我最好的猜测是 JIT 设法不断折叠本地代码,当它在自己的模块中时可能不太好。您可能对how to look at what the jit has done 感兴趣
  • @Alex 你能发布一个可以运行的示例,以便我们自己尝试基准测试吗?
  • @soegaard 他们应该准备好运行了,你只需在顶部添加#lang racket。
  • 我无法提供任何解决方案或答案,但我可以确认 Eratosthenes 筛的 Racket 流实现非常缓慢、令人无法接受!您使用的算法非常标准(参见例如 SICP);比较它,例如对于 MIT-Scheme,找到第 500 个素数时速度要慢 100 倍! i7 3分钟?真的吗?将其与 Clojure 进行比较只会导致绝望。我认为有些严重的错误。不能按原样使用。但也许我们都遗漏了一些明显的东西......
  • 这不仅仅是速度。空间需求也很荒谬。必须将内存限制设置为 4096 MB 才能计算出第 500 个素数 (3581)。我会在某个阶段对其进行研究,但就目前而言,它并不真正适合目的。

标签: performance racket sieve-of-eratosthenes


【解决方案1】:

这是一个观察:

使用打印数字的integers-starting-from-stream 版本 生成时:

(define (integers-starting-from-stream n)
  (stream-cons n
               (begin
                 (display (~a n " "))
                 (integers-starting-from-stream (+ n 1)))))

同样:

(define (integers-starting-from n)
  (s-cons n
          (begin (display (~a n " "))
                 (integers-starting-from (+ n 1)))))

我们可以测试:

(collect-garbage) (collect-garbage) (collect-garbage)
(time (stream-limit (sieve x) 10))
(collect-garbage) (collect-garbage) (collect-garbage)
(time (s-limit (s-sieve s-x) 10))

我们观察到流版本打印了从 2 到 51 的数字 其中 s 版本打印从 2 到 30 的数字。

流版本生成的列表几乎是两倍大小。

这是流版本比自定义版本慢的第一个(也是最重要的)原因。

流版本较慢的第二个原因是流版本缓存了 stream-first 的结果。当元素的计算速度很慢时,缓存通常会更快。

(define (integers-starting-from-stream n)
  (stream-cons (begin (sleep 1) n)
               (integers-starting-from-stream (+ n 1))))

(define (integers-starting-from n)
  (s-cons (begin (sleep 1) n)
          (integers-starting-from (+ n 1))))

然后运行:

(collect-garbage) (collect-garbage) (collect-garbage)
(define x (integers-starting-from-stream 2))
(time (stream-limit x 10))
(time (stream-limit x 10))
(collect-garbage) (collect-garbage) (collect-garbage)
(define s-x (integers-starting-from 2))
(time (s-limit s-x 10))
(time (s-limit s-x 10))

【讨论】:

  • 你有什么解释,为什么流列表会是两倍,对于更大的值甚至超过两倍的大小?如果我理解正确,您是说流缓存了这些值,但如果没有,它会更快吗?
  • @Alex Caching 有额外的代码需要执行并且增加了内存使用。好处是当您执行stream-rest 时,先前计算的步骤将被缓存,因此第二次运行的代码是一个if 和一个绑定评估。但是,您永远不会多次访问流部分,因此在这种情况下缓存没有任何好处。
  • @alex 除了始终评估第一个元素与延迟第一个元素之间的区别之外。尝试使用integers-starting-from-streamstream-limit,看看是否可以让它们生成相同长度的列表。
猜你喜欢
  • 2013-11-22
  • 2012-10-14
  • 2020-06-19
  • 2019-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-07
  • 1970-01-01
相关资源
最近更新 更多