-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathversioned-objects.lisp
More file actions
416 lines (347 loc) · 18.2 KB
/
Copy pathversioned-objects.lisp
File metadata and controls
416 lines (347 loc) · 18.2 KB
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
(defpackage :versioned-objects
(:use :cl :bt :modf :iterate)
(:import-from modf find-container)
(:export #:version
#:versioned-object
#:vfuncall #:vapply
#:with-versioned-object #:with-versioned-objects
#:vmodf ))
(in-package :versioned-objects)
;; (defmacro cond-lock-held (&rest lock-clauses)
;; (if (car lock-clauses)
;; `(if (bt:acquire-lock ,(caar lock-clauses) nil)
;; (unwind-protect
;; (progn ,@(cdar lock-clauses))
;; (bt:release-lock ,(caar lock-clauses)) )
;; (cond-lock-held ,(cdr lock-clauses)) )))
;; (cond-lock-held (*modifier-lock* t)
;; (*read-only-lock* t) )
;; @\section{introduction}
;; @Versioned-objects is a library that brings general verioning to Common Lisp.
;; This allows you to use arbitrary data structures in a functional way without
;; facing the cost of copying the data structure on modification. Versioning of
;; data structures is a method that has very attractive performance (usually
;; constant time) in many use cases such as in backtracking search.
;; @This can be viewed as a sister project of Modf, another way to achieve
;; functional data manipulation in Common Lisp. In many ways it achieves the
;; same goals as Modf but via drastically different mechanisms.
;; @1. Modf works well when the data structure you are using supports functional
;; manipulation while Versioned-objects only works for data with defined {\em
;; setf} expansions but performance determined by how quickly the data can be
;; accessed and mutated, not functionally modified.
;; @2. Modf allows you to work with the native datatypes in Common Lisp meaning
;; your data will work in any function without further consideration while
;; Versioned-objects relies of wrapping the data, and thus special care must be
;; taken when calling functions with your versioned data as arguments.
;; @3. Modf returns objects that may share memory, but are completely independent
;; otherwise, thereby making them perfect for multithreaded programs.
;; Versioned-objects creates a network of interconnected data which, other than
;; using a few special methods, cannot be used in any asynchronous fashion at
;; all (the structure needs to be locked access by only one thread at a time,
;; even for reads).
;; @4. Modf must resort to copying on any object without persistent
;; characteristics. Versioned-objects only relies on copying when it is faster
;; to do so than use the versioning mechanism.
;; @In general, these two libraries overlap in functionality, but are useful at
;; two different limits of the data spectrum. Modf is useful when objects are
;; small (and thus cheap to copy) or have persistent characteristics (like
;; singly linked lists or binary trees) or are involved in parallel execution.
;; Versioned-objects is useful when objects are large (and thus expensive to
;; copy) and have no persistent characteristics and are not frequently accessed
;; by multiple threads.
;; @\subsection{Simple Usage}
;; @Using Versioned-objects is simple. You take any object and call the function
;; <<version>> on it. This will transform the object into a versioned-object.
;; In general, the object passed to <<version>> should never be accessed or
;; mutated after calling <<version>>. At this point you may make cheap copies
;; that require no more space than the different between the original and the
;; new copy. There are a few options you can pass to <<version>> that will
;; determine how the version tree is maintained, but we'll leave that for later.
;; @To access the data in your versioned-object, you must use the functions
;; <<vfuncall>> and <<vapply>> which work precisely as their {\em sans v}
;; counterparts, but work with versioned data in their argument lists. Later on
;; we will see how to define your own accessor functions which may be faster and
;; possibly more aesthetically pleasing, but for now we leave it at this.
;; @\section{Implementation}
;; @The structure <<versioned-object>> is a cons-cell like structure that makes
;; up the version tree for your object.
;;<<versioned-object>>=
(defstruct (versioned-object (:constructor %make-versioned-object)
(:conc-name :vo-) )
car cdr lock )
;; @The function <<version>> takes an object and wraps it in a
;; <<versioned-object>> structure if it is not already a versioned object. This
;; means that it is not possible to version the <<versioned-object>> structure.
;;<<>>=
(defun version (object)
"Convert an object into a versioned object."
(if (versioned-object-p object)
object
(%make-versioned-object :car object
:lock (bt:make-lock) )))
;; @To put this all in not very clear language, this basically works like this.
;; A versioned object is a list whose last element is the object. When you
;; access a value from the object, the object moves to the first element of the
;; list (making it a list of length 1) while at the same time reversing the list
;; after it and inverting the deltas.
;; @The `function' <<raise-object!>> makes your version of the object current.
;; This involves a lot of mutation and thus a lock is held.
;;<<>>=
(defun raise-object! (v-obj)
"Bubble object to beginning of list, along the way back, reverse the list.
This assumes that locks are already held."
(if (not (vo-cdr v-obj))
nil ; This value is ignored
(progn
(raise-object! (vo-cdr v-obj))
(let ((car (vo-car v-obj)))
(destructuring-bind (new-val getter setter)
car;;(vo-car v-obj)
;; Move the object
(setf (vo-car v-obj)
(vo-car (vo-cdr v-obj)) )
;; Invert delta
(setf (vo-car (vo-cdr v-obj))
(list (funcall getter)
getter setter ))
;; Mutate object
(funcall setter new-val)
;; Reverse the list
(setf (vo-cdr (vo-cdr v-obj))
v-obj )
;; Terminate the list
(setf (vo-cdr v-obj) nil) )))))
;; @The macro <<vmodf>> acts as the main entry point. It has the same syntax as
;; <<modf>> except no special treatment for <<modf-eval>>.
;; <<>>=
(defmacro vmodf (place value &rest more)
"Add a new entry ot the version tree of the underlying container and return
that new version of the object. Several place/value pairs can be given. In
between each pair, a symbol must be given to specify where the result of the
previous calculation should be bound for the rest of the VMODF place/value
pairs."
;; First we need to find the "container" variable
(let ((container (find-container place)))
(alexandria:with-gensyms (val-sym v-obj new-version container-sym)
`(let* ((,val-sym ,value)
(,v-obj ,container)
(,new-version
(bt:with-lock-held ((vo-lock ,container))
(raise-object! ,container)
(let* ((,container-sym (vo-car ,v-obj))
(getter (lambda ()
(let ((,container ,container-sym))
,place )))
(setter (lambda (new-val)
(let ((,container ,container-sym))
(setf ,place new-val) )))
;; Grab the old value
(old-value (funcall getter))
(delta (list old-value getter setter)) )
;; Set the new value
(funcall setter ,val-sym)
;; Make a new versioned object with the change
(setf (vo-cdr ,v-obj) (%make-versioned-object
:car ,container-sym
:cdr nil
:lock (vo-lock ,v-obj) )
(vo-car ,v-obj) delta )
;; return that new object
(vo-cdr ,v-obj) ))))
,(if more `(let ((,(first more) ,new-version))
(vmodf ,@(cdr more)) )
new-version )))))
;; @\section{Accessing data in a versioned object}
;; @I'm sorry to say, a versioned object isn't just a wrapper that magically
;; delivers new functionality. Due to the inherent frailty that comes with data
;; mutation, accessing the data within a versioned object must be done with the
;; utmost of caution. With this in mind the interface for accessing the data in
;; versioned objects is a bit convoluted. We provide two functions,
;; <<vfuncall>> and <<vapply>> which you may call and use your versioned
;; structures as arguments. This allows you to do most of what you probably
;; want to do.
(defun vfuncall (&rest args)
"Like FUNCALL except you can use versioned structures. Call the function
specified by the first argument with the rest of the arguments as its argument
list."
(let ((held-locks))
(unwind-protect
(let ((new-args
(iter (for arg in args)
(collecting
(cond ((versioned-object-p arg)
(bt:acquire-lock (vo-lock arg))
(push (vo-lock arg) held-locks)
(raise-object! arg)
(vo-car arg) )
(t arg) )))))
(apply #'funcall new-args) )
(iter (for lock in held-locks)
(bt:release-lock lock) ))))
(defun vapply (fn &rest args)
(apply #'vfuncall fn (apply #'list* args)))
;; @We recognize that your data is less useful when it is locked into a
;; <<verioned-object>> structure, so we allow for a way to temporarily retrieve
;; it. In order to get the raw data, use the macros <<with-versioned-object>>
;; and <<with-versioned-objects>>. You may pass this underlying data to another
;; function that expects the raw object, but the object needs to be locked for
;; the duration of that function call and that function should not modify the
;; values in the object.
;; @The macros <<with-versioned-object>> and <<with-versioned-objects>> are a
;; couple macros that allow you to access your versioned data. These will make
;; the version you are referencing current and bind the variable to the
;; underlying data. A lock is held while inside this environment, so be wary of
;; deadlock issues (I am working on something to reduce the chances of deadlock
;; to a mere performance penalty).
;;<<>>=
(defmacro with-versioned-object (object &body body)
`(bt:with-lock-held ((vo-lock ,object))
(raise-object! ,object)
(let ((,object (vo-car ,object)))
,@body )))
;;<<>>=
(defmacro with-versioned-objects (objects &body body)
(if objects
`(with-versioned-object ,(car objects)
(with-versioned-objects ,(cdr objects)
,@body ))
`(progn ,@body) ))
;; @\section{Printing}
;; @The printing method needs to make the specified version current, then it
;; prints it like any other object.
(defvar *versioned-object-printer* t
"When T, objects are printed specially as \"#<Versioned <OBJECT>>\". If NIL
then the true object is printed (a structure) whose representation can be quite
large. This is useful for debugging as the special printer, in general, alters
the data structure." )
;;<<>>=
(defmethod print-object ((obj versioned-object) stream)
(if *versioned-object-printer*
(vfuncall 'format stream "#<Versioned ~A>" obj)
(call-next-method) ))
;; @\section{Thread Safety}
;; @There are pretty serious concerns about deadlock. If there are two threads
;; each accessing two version of the same data, and one is reading from an
;; object and needs to communicate with the other thread which also needs to
;; accessing the data, we have a classic deadlock situation. This is
;; particularly dangerous here because even reads require locking the data which
;; most people take to be a non-locking operation.
;; @\section{Random Thoughts}
;; @vfuncall and vapply
;; @Eval arguments
;; @After evaluation, run through arguments and lock on any versioned structures
;; using the main object lock.
;; @In case of deadlock, try to use lock on the in-place object lock and use
;; in-place access and versioning tools to move ahead. If all else fails, bail
;; out.
;; @Many in-place changes can happen at once, however it seems that the any
;; in-place operation is dangerous if the main lock is released. This means
;; that we should either make the.
;; @We provide special treatment for arrays, hash tables, classes, and
;; structures. Other objects can either have special functions defined for
;; them, or then can use the general interface which is slower and perhaps more
;; prone to failure (we'll see).
;; @\subsection{Why not implement it as a generalization of Modf?}
;; @While this could, in principle, be done, it is important to note that
;; Versioned-objects breaks the basic assumptions that make Modf work. This is
;; because the mechanism which it uses is a completely different one. For
;; instance, it never builds new objects, the primary purpose of Modf. I
;; considered implementing it as a special syntax within Modf for the sake of
;; combining similar purposes if not similar mechanisms, but decided that this
;; approach would lead to a lot of confusion. Versioned objects are inherently
;; tied to a central piece of data wrapped in a versioning data structure. This
;; just doesn't jive with the mindset of Modf. Further, there is no way to use
;; the Mod functionality and the Versioned-objects functionality at the same
;; time. While it makes sense to say set the value of the n$^{th}$ element of a
;; versioned array to a Modf constructed new object, it cannot be done as a
;; single generalized Modf statement in any syntax I can think of.
;; @\subsection{Versioned objects are tricky}
;; @Take, for instance, the expression:
;; (let* ((arr1 (version (make-array '(10) :initial-element 0)))
;; (arr2 (vmodf (aref arr1 0) 5)) )
;; (aref arr1 (aref arr2 0)) )
;; @It is impossible to retrofit <<compare-arrays>> as a function that can use
;; versioned data. This is because the access happens within the function,
;; completely out of our control. Thus {\em arr1} and {\em arr2} must be Lisp
;; arrays yet they refer to the same array. In such a case, it seems that a
;; copy is the only way to deal with this.
;; (defun compare-arrays (arr1 arr2)
;; (iter
;; (for val1 in-sequence arr1)
;; (for val2 in-sequence arr2)
;; (always (eql val1 val2)) ))
;; (let* ((arr1 (version (make-array '(10) :initial-element 0)))
;; (arr2 (vmodf (aref arr1 0) 5)) )
;; (compare-arrays arr1 arr2) )
;; @We must evaluate all the other arguments prior to the argument that contains
;; the versioned object. These arguments might include other references to
;; other versions of the same object.
;; (defmacro version-access (form)
;; (let ((cont-sym (find-container form)))
;; `(with-versioned-object ,cont-sym
;; ,form )))
;; @\section{Benchmarking}
;; @Usually I would aschew focusing on benchmarks, but because of the subtle
;; nature of what is happening in this library, and the many ways that it could
;; be implemented, it seems necessary to understand in realistic terms how long
;; certain operations take.
;; @\subsection{Rebasing}
;; @The original functionality of the library basically rebased the version tree
;; at each access such that the version you are accessing is the root. Here we
;; measure how long that takes as a function of $m$, the number of edits between
;; two versions. We do this for arrays, hash tables, class instances, and
;; structure instances, four data structures where this library will likely find
;; the most use.
;; (defmacro time-operation ((&key (repeat 10000) (min-sample-time 5))
;; &body body )
;; `(let* ((start-time (get-internal-real-time))
;; (end-time start-time)
;; (times (iter
;; (for times from 0)
;; (until (> (setf end-time (get-internal-real-time))
;; (+ start-time
;; (* ,min-sample-time
;; internal-time-units-per-second ))))
;; (iter (for i below ,repeat)
;; ,@body )
;; (finally (return times)) )))
;; (float
;; (/ (* ,repeat times) (/ (- end-time start-time)
;; internal-time-units-per-second )) 0d0)))
;; This doesn't work. Why? The printer!?!
;; (defun measure-rebase-time (n-edits)
;; "Measure how long it takes to rebase the array."
;; (let* ((arr1 (version (make-array '(10) :initial-element 0)))
;; (arr2 (iter (for i below n-edits)
;; (for new-arr
;; initially (vmodf (aref arr1 (random 10)) (random 10))
;; then (vmodf (aref new-arr (random 10)) (random 10)) )
;; (finally (return new-arr)) )))
;; (let ((rebases-per-second (time-operation ()
;; (raise-object! arr1)
;; (raise-object! arr2) )))
;; (values rebases-per-second
;; ;; Deltas per second
;; (* rebases-per-second n-edits) ))))
;; @Preliminary results suggest that using a more speciallized approach to
;; versioning can push rebase speed up by ~40%.
;; (defun measure-version-time (n-versions-before-rebase)
;; "Measure how much time it takes to create new versions of the data if building
;; off of the most recent version."
;; (let ((arr1 (version (make-array '(10) :initial-element 0))))
;; (let ((versions-per-second
;; (time-operation ()
;; (iter (for i below n-versions-before-rebase)
;; (for new-arr
;; initially (vmodf (aref arr1 (random 10)) (random 10))
;; then (vmodf (aref new-arr (random 10)) (random 10)) )
;; (finally (return new-arr)) ))))
;; (values versions-per-second) )))
;; (defun measure-in-place-walk-time (n-edits)
;; "Measure how long it takes to read a value from the datastructure without
;; rebasing it."
;; )
;; (defun measure-in-place-version-time (n-edits)
;; "Measure how long it takes to add a new version to the data structure without
;; rebasing it. This implicitly includes a read."
;; )