我认为值得指出的是,在这种情况下分析空间使用情况并不总是那么简单。
例如,这里是 Racket 中 force 和 delay 的一个完全幼稚的实现:
(define-syntax-rule (delay form)
(λ () form))
(define (force p)
(p))
而且我们可以构建足够多的与 SICP 流有点兼容的东西,从而在这方面造成危险:
(define-syntax-rule (cons-stream kar kdr)
;; Both car & cdr can be delayed: why not? I think the normal thing is
;; just to delay the cdr
(cons (delay kar) (delay kdr)))
(define (stream-car s)
(force (car s)))
(define (stream-cdr s)
(force (cdr s)))
(define (stream-nth s n)
(if (zero? n)
(stream-car s)
(stream-nth (stream-cdr s) (- n 1))))
(请注意这里缺少很多,因为我很懒。)
在此基础上,我们可以构建整数流:
(define (integers-starting-from n)
(cons-stream n (integers-starting-from (+ n 1))))
现在我们可以试试这个:
(define naturals (integers-starting-from 0))
(stream-nth naturals 10000000)
过了一会儿,最后这件事返回10000000。我们可以多次调用它,每次都得到相同的答案。
但是我们对 Promise 的实现很糟糕:强制一个 Promise 让它工作每次我们强制它,我们想只做一次。相反,我们可以记住我们的承诺,这样就不会发生这种情况(这可能不是线程安全的:可以这样做):
(define-syntax-rule (delay form)
(let ([thunk/value (λ () form)]
[forced? #f])
(λ ()
(if forced?
thunk/value
(let ([value (thunk/value)])
(set! thunk/value value)
(set! forced? #t)
value)))))
其余的代码都是一样的。
现在,当您致电(stream-nth naturals 10000000) 时,您可能会遇到相当糟糕的情况:尤其是您可能会耗尽内存。
你会过得不好的原因有两个:
- 以
naturals 的形式引用整个流;
- 花哨的 Promise 正在记住它们的值,这是整个流的尾部。
这意味着,当你沿着流向下走时,你会消耗越来越多的内存,直到你用完:程序的空间复杂度就像最后一行 stream-nth 的参数大小一样。
这里的问题是delay 试图以一种在这种情况下无益的方式变得聪明。特别是如果您将流视为您通常会遍历一次的对象,那么记住它们是没有用的:您已经仔细记住了一个永远不会再次使用的值。
Racket memoize 提供的delay 和force 的版本,在这种情况下也会占用大量内存。
您可以避免这种情况,方法是不记忆,或者确保永远不要保留流的开头,以便 GC 可以拾取它。特别是这个程序
(define (silly-nth-natural n)
(define naturals (integers-starting-from 0))
(stream-nth naturals n))
不会使用与n 成比例的空间,因为一旦对stream-nth 进行了第一次尾调用,就不再有任何东西保留流的开头。
另一种方法是只弱保存记忆值,这样如果系统绝望,它可以丢弃它。这是一个 hacky 且大部分未经测试的实现(这是非常特定于 Racket 的):
(define-syntax-rule (delay form)
;; a version of delay which memoizes weakly
(let ([thunk (λ () form)]
[value-box #f])
(λ ()
(if value-box
;; the promise has been forced
(let ([value-maybe (weak-box-value value-box value-box)])
;; two things that can't be in the box are the thunk
;; or the box itself, since we made those ourselves
(if (eq? value-maybe value-box)
;; the value has been GCd
(let ([value (thunk)])
(set! value-box (make-weak-box value))
value)
;; the value is good
value-maybe))
;; the promise has not yet been forced
(let ((value (thunk)))
(set! value-box (make-weak-box value))
value)))))
我怀疑大量的weak box可能会让GC做很多工作。