aboutsummaryrefslogtreecommitdiff
path: root/guix/memoization.scm
blob: 086c4cdc56b00193068f6d06225338dfbc946949 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (guix memoization)
  #:use-module (guix profiling)
  #:use-module (ice-9 match)
  #:autoload   (srfi srfi-1) (count)
  #:use-module (srfi srfi-9)
  #:export (invalidate-memoization!
            memoize
            mlambda
            mlambdaq))

;; Data type representation a memoization cache when profiling is on.
(define-record-type <cache>
  (make-cache table lookups hits)
  cache?
  (table   cache-table)
  (lookups cache-lookups set-cache-lookups!)
  (hits    cache-hits    set-cache-hits!))

(define-syntax-rule (define-lookup-procedure proc get)
  "Define a lookup procedure PROC.  When profiling is turned off, PROC is set
to GET; when profiling is on, PROC is a wrapper around GET that keeps tracks
of lookups and cache hits."
  (define proc
    (if (profiled? "memoization")
        (lambda (cache key default)
          (let ((result (get (cache-table cache) key default)))
            (set-cache-lookups! cache (+ 1 (cache-lookups cache)))
            (unless (eq? result default)
              (set-cache-hits! cache (+ 1 (cache-hits cache))))
            result))
        get)))

(define-syntax-rule (define-update-procedure proc put!)
  "Define an update procedure PROC.  When profiling is turned off, PROC is
equal to PUT!; when profiling is on, PROC is a wrapper around PUT and unboxes
the underlying hash table."
  (define proc
    (if (profiled? "memoization")
        (lambda (cache key value)
          (put! (cache-table cache) key value))
        put!)))

(define-lookup-procedure cache-ref hash-ref)
(define-lookup-procedure cacheq-ref hashq-ref)
(define-update-procedure cache-set! hash-set!)
(define-update-procedure cacheq-set! hashq-set!)

(define-syntax-rule (call/mv thunk)
  (call-with-values thunk list))
(define-syntax-rule (return/mv lst)
  (apply values lst))

(define-syntax-rule (call/1 thunk)
  (thunk))
(define-syntax-rule (return/1 value)
  value)

(define-syntax define-cache-procedure
  (syntax-rules ()
    "Define a procedure NAME that implements a cache using HASH-REF and
HASH-SET!.  Use CALL to invoke the thunk and RETURN to return its value; CALL
and RETURN are used to distinguish between multiple-value and single-value
returns."
    ((_ name hash-ref hash-set! call return)
     (define name
       (let ((%nothing '(this is nothing)))
         (lambda (cache key thunk)
           "Cache the result of THUNK under KEY in CACHE, or return the
already-cached result."
           (let ((results (hash-ref cache key %nothing)))
             (if (eq? results %nothing)
                 (let ((results (call thunk)))
                   (hash-set! cache key results)
                   (return results))
                 (return results)))))))
    ((_ name hash-ref hash-set!)
     (define-cache-procedure name hash-ref hash-set!
       call/mv return/mv))))

(define-cache-procedure cached/mv  cache-ref cache-set!)
(define-cache-procedure cachedq/mv cacheq-ref cacheq-set!)
(define-cache-procedure cached  cache-ref cache-set! call/1 return/1)
(define-cache-procedure cachedq cacheq-ref cacheq-set! call/1 return/1)

(define %memoization-tables
  ;; Map procedures to the underlying hash table.
  (make-weak-key-hash-table))

(define %make-hash-table*
  ;; When profiling is off, this is equivalent to 'make-hash-table'.  When
  ;; profiling is on, return a hash table wrapped in a <cache> object.
  (if (profiled? "memoization")
      (lambda (proc location)
        (let ((cache (make-cache (make-hash-table) 0 0)))
          (hashq-set! %memoization-tables proc
                      (cons cache location))
          cache))
      (lambda (proc location)
        (let ((table (make-hash-table)))
          (hashq-set! %memoization-tables proc table)
          table))))

(define-syntax-rule (make-hash-table* proc)
  (%make-hash-table* proc (current-source-location)))

(define (invalidate-memoization! proc)
  "Invalidate the memoization cache of PROC."
  (match (hashq-ref %memoization-tables proc)
    ((? hash-table? table)
     (hash-clear! table))
    (((? cache? cache) . _)
     (hash-clear! (cache-table cache)))))

(define* (show-memoization-tables #:optional (port (current-error-port)))
  "Display to PORT statistics about the memoization tables."
  (define (cache<? p1 p2)
    (match p1
      ((cache1 . _)
       (match p2
         ((cache2 . _)
          (< (hash-count (const #t) (cache-table cache1))
             (hash-count (const #t) (cache-table cache2))))))))

  (define caches
    (hash-map->list (lambda (key value)
                      value)
                    %memoization-tables))

  (match (sort caches (negate cache<?))
    (((caches . locations) ...)
     (format port "Memoization: ~a tables, ~a non-empty~%"
             (length caches)
             (count (lambda (cache)
                      (> (hash-count (const #t) (cache-table cache)) 0))
                    caches))
     (for-each (lambda (cache location)
                 (let ((size (hash-count (const #t) (cache-table cache))))
                   (unless (zero? size)
                     (format port "  ~a:~a:~a: \t~a entries, ~a lookups, ~a% hits~%"
                             (assq-ref location 'filename)
                             (and=> (assq-ref location 'line) 1+)
                             (assq-ref location 'column)
                             size
                             (cache-lookups cache)
                             (inexact->exact
                              (round
                               (* 100. (/ (cache-hits cache)
                                          (cache-lookups cache) 1.))))))))
               caches locations))))

(register-profiling-hook! "memoization" show-memoization-tables)

(define (memoize proc)
  "Return a memoizing version of PROC.

This is a generic version of 'mlambda' what works regardless of the arity of
'proc'.  It is more expensive since the argument list is always allocated, and
the result is returned via (apply values results)."
  (letrec* ((mproc (lambda args
                     (cached/mv cache args
                                (lambda ()
                                  (apply proc args)))))
            (cache (make-hash-table* mproc)))
    mproc))

(define-syntax %mlambda
  (syntax-rules ()
    "Return a memoizing lambda.  This is restricted to procedures that return
exactly one value."
    ((_ cached () body ...)
     ;; The zero-argument case is equivalent to a promise.
     (let ((result #f) (cached? #f)
           (compute (lambda () body ...)))
       (lambda ()
         (unless cached?
           (set! result (compute))
           (set! cached? #t))
         result)))

    ;; Optimize the fixed-arity case such that there's no argument list
    ;; allocated.  XXX: We can't really avoid the closure allocation since
    ;; Guile 2.0's compiler will always keep it.
    ((_ cached (arg) body ...)                    ;one argument
     (letrec* ((proc  (lambda (arg) body ...))
               (mproc (lambda (arg)
                        (cached cache arg (lambda () (proc arg)))))
               (cache (make-hash-table* mproc)))
       mproc))
    ((_ _ (args ...) body ...)                    ;two or more arguments
     (letrec* ((proc  (lambda (args ...) body ...))
               (mproc (lambda (args ...)
                        ;; XXX: Always use 'cached', which uses 'equal?', to
                        ;; compare the argument lists.
                        (cached cache (list args ...)
                                (lambda ()
                                  (proc args ...)))))
               (cache (make-hash-table* mproc)))
       mproc))))

(define-syntax-rule (mlambda formals body ...)
  "Define a memoizing lambda.  The lambda's arguments are compared with
'equal?', and BODY is expected to yield a single return value."
  (%mlambda cached formals body ...))

(define-syntax-rule (mlambdaq formals body ...)
  "Define a memoizing lambda.  If FORMALS lists a single argument, it is
compared using 'eq?'; otherwise, the argument list is compared using 'equal?'.
BODY is expected to yield a single return value."
  (%mlambda cachedq formals body ...))