memoization.scm 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2017 Ludovic Courtès <ludo@gnu.org>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  6. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; GNU Guix is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;;; GNU General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. (define-module (guix memoization)
  19. #:use-module (guix profiling)
  20. #:use-module (ice-9 match)
  21. #:autoload (srfi srfi-1) (count)
  22. #:use-module (srfi srfi-9)
  23. #:export (invalidate-memoization!
  24. memoize
  25. mlambda
  26. mlambdaq))
  27. ;; Data type representation a memoization cache when profiling is on.
  28. (define-record-type <cache>
  29. (make-cache table lookups hits)
  30. cache?
  31. (table cache-table)
  32. (lookups cache-lookups set-cache-lookups!)
  33. (hits cache-hits set-cache-hits!))
  34. (define-syntax-rule (define-lookup-procedure proc get)
  35. "Define a lookup procedure PROC. When profiling is turned off, PROC is set
  36. to GET; when profiling is on, PROC is a wrapper around GET that keeps tracks
  37. of lookups and cache hits."
  38. (define proc
  39. (if (profiled? "memoization")
  40. (lambda (cache key default)
  41. (let ((result (get (cache-table cache) key default)))
  42. (set-cache-lookups! cache (+ 1 (cache-lookups cache)))
  43. (unless (eq? result default)
  44. (set-cache-hits! cache (+ 1 (cache-hits cache))))
  45. result))
  46. get)))
  47. (define-syntax-rule (define-update-procedure proc put!)
  48. "Define an update procedure PROC. When profiling is turned off, PROC is
  49. equal to PUT!; when profiling is on, PROC is a wrapper around PUT and unboxes
  50. the underlying hash table."
  51. (define proc
  52. (if (profiled? "memoization")
  53. (lambda (cache key value)
  54. (put! (cache-table cache) key value))
  55. put!)))
  56. (define-lookup-procedure cache-ref hash-ref)
  57. (define-lookup-procedure cacheq-ref hashq-ref)
  58. (define-update-procedure cache-set! hash-set!)
  59. (define-update-procedure cacheq-set! hashq-set!)
  60. (define-syntax-rule (call/mv thunk)
  61. (call-with-values thunk list))
  62. (define-syntax-rule (return/mv lst)
  63. (apply values lst))
  64. (define-syntax-rule (call/1 thunk)
  65. (thunk))
  66. (define-syntax-rule (return/1 value)
  67. value)
  68. (define-syntax define-cache-procedure
  69. (syntax-rules ()
  70. "Define a procedure NAME that implements a cache using HASH-REF and
  71. HASH-SET!. Use CALL to invoke the thunk and RETURN to return its value; CALL
  72. and RETURN are used to distinguish between multiple-value and single-value
  73. returns."
  74. ((_ name hash-ref hash-set! call return)
  75. (define name
  76. (let ((%nothing '(this is nothing)))
  77. (lambda (cache key thunk)
  78. "Cache the result of THUNK under KEY in CACHE, or return the
  79. already-cached result."
  80. (let ((results (hash-ref cache key %nothing)))
  81. (if (eq? results %nothing)
  82. (let ((results (call thunk)))
  83. (hash-set! cache key results)
  84. (return results))
  85. (return results)))))))
  86. ((_ name hash-ref hash-set!)
  87. (define-cache-procedure name hash-ref hash-set!
  88. call/mv return/mv))))
  89. (define-cache-procedure cached/mv cache-ref cache-set!)
  90. (define-cache-procedure cachedq/mv cacheq-ref cacheq-set!)
  91. (define-cache-procedure cached cache-ref cache-set! call/1 return/1)
  92. (define-cache-procedure cachedq cacheq-ref cacheq-set! call/1 return/1)
  93. (define %memoization-tables
  94. ;; Map procedures to the underlying hash table.
  95. (make-weak-key-hash-table))
  96. (define %make-hash-table*
  97. ;; When profiling is off, this is equivalent to 'make-hash-table'. When
  98. ;; profiling is on, return a hash table wrapped in a <cache> object.
  99. (if (profiled? "memoization")
  100. (lambda (proc location)
  101. (let ((cache (make-cache (make-hash-table) 0 0)))
  102. (hashq-set! %memoization-tables proc
  103. (cons cache location))
  104. cache))
  105. (lambda (proc location)
  106. (let ((table (make-hash-table)))
  107. (hashq-set! %memoization-tables proc table)
  108. table))))
  109. (define-syntax-rule (make-hash-table* proc)
  110. (%make-hash-table* proc (current-source-location)))
  111. (define (invalidate-memoization! proc)
  112. "Invalidate the memoization cache of PROC."
  113. (match (hashq-ref %memoization-tables proc)
  114. ((? hash-table? table)
  115. (hash-clear! table))
  116. (((? cache? cache) . _)
  117. (hash-clear! (cache-table cache)))))
  118. (define* (show-memoization-tables #:optional (port (current-error-port)))
  119. "Display to PORT statistics about the memoization tables."
  120. (define (cache<? p1 p2)
  121. (match p1
  122. ((cache1 . _)
  123. (match p2
  124. ((cache2 . _)
  125. (< (hash-count (const #t) (cache-table cache1))
  126. (hash-count (const #t) (cache-table cache2))))))))
  127. (define caches
  128. (hash-map->list (lambda (key value)
  129. value)
  130. %memoization-tables))
  131. (match (sort caches (negate cache<?))
  132. (((caches . locations) ...)
  133. (format port "Memoization: ~a tables, ~a non-empty~%"
  134. (length caches)
  135. (count (lambda (cache)
  136. (> (hash-count (const #t) (cache-table cache)) 0))
  137. caches))
  138. (for-each (lambda (cache location)
  139. (let ((size (hash-count (const #t) (cache-table cache))))
  140. (unless (zero? size)
  141. (format port " ~a:~a:~a: \t~a entries, ~a lookups, ~a% hits~%"
  142. (assq-ref location 'filename)
  143. (and=> (assq-ref location 'line) 1+)
  144. (assq-ref location 'column)
  145. size
  146. (cache-lookups cache)
  147. (inexact->exact
  148. (round
  149. (* 100. (/ (cache-hits cache)
  150. (cache-lookups cache) 1.))))))))
  151. caches locations))))
  152. (register-profiling-hook! "memoization" show-memoization-tables)
  153. (define (memoize proc)
  154. "Return a memoizing version of PROC.
  155. This is a generic version of 'mlambda' what works regardless of the arity of
  156. 'proc'. It is more expensive since the argument list is always allocated, and
  157. the result is returned via (apply values results)."
  158. (letrec* ((mproc (lambda args
  159. (cached/mv cache args
  160. (lambda ()
  161. (apply proc args)))))
  162. (cache (make-hash-table* mproc)))
  163. mproc))
  164. (define-syntax %mlambda
  165. (syntax-rules ()
  166. "Return a memoizing lambda. This is restricted to procedures that return
  167. exactly one value."
  168. ((_ cached () body ...)
  169. ;; The zero-argument case is equivalent to a promise.
  170. (let ((result #f) (cached? #f)
  171. (compute (lambda () body ...)))
  172. (lambda ()
  173. (unless cached?
  174. (set! result (compute))
  175. (set! cached? #t))
  176. result)))
  177. ;; Optimize the fixed-arity case such that there's no argument list
  178. ;; allocated. XXX: We can't really avoid the closure allocation since
  179. ;; Guile 2.0's compiler will always keep it.
  180. ((_ cached (arg) body ...) ;one argument
  181. (letrec* ((proc (lambda (arg) body ...))
  182. (mproc (lambda (arg)
  183. (cached cache arg (lambda () (proc arg)))))
  184. (cache (make-hash-table* mproc)))
  185. mproc))
  186. ((_ _ (args ...) body ...) ;two or more arguments
  187. (letrec* ((proc (lambda (args ...) body ...))
  188. (mproc (lambda (args ...)
  189. ;; XXX: Always use 'cached', which uses 'equal?', to
  190. ;; compare the argument lists.
  191. (cached cache (list args ...)
  192. (lambda ()
  193. (proc args ...)))))
  194. (cache (make-hash-table* mproc)))
  195. mproc))))
  196. (define-syntax-rule (mlambda formals body ...)
  197. "Define a memoizing lambda. The lambda's arguments are compared with
  198. 'equal?', and BODY is expected to yield a single return value."
  199. (%mlambda cached formals body ...))
  200. (define-syntax-rule (mlambdaq formals body ...)
  201. "Define a memoizing lambda. If FORMALS lists a single argument, it is
  202. compared using 'eq?'; otherwise, the argument list is compared using 'equal?'.
  203. BODY is expected to yield a single return value."
  204. (%mlambda cachedq formals body ...))