Skip to content

Instantly share code, notes, and snippets.

@dkochmanski
Created June 3, 2020 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dkochmanski/959ec9ea865ea5e53c58c154f936fcb6 to your computer and use it in GitHub Desktop.
Save dkochmanski/959ec9ea865ea5e53c58c154f936fcb6 to your computer and use it in GitHub Desktop.

Conformal displacement sketch {#conformal-displacement-sketch}

We will now take a pity on the CPU and the memory. We excessively call adjust-array and cons a new list for each written character. We'd like to have arrays with a two-dimensional fill pointer instead and to adjust the array only when it is too small.

Since we are at it, we'll make this array wrapper allow a conformal displacement. We'll sketch an implementation which introduces its own operators, but this could be an extension to Common Lisp, for instance Genera had such a feature.

Conformal displacement allows displacing an array into a slice of the another one (instead of simply offsetting the memory access). Consider the following example:

CL-USER> (defparameter *arr* (make-array '(16 16) :initial-element 0))
*ARR*
CL-USER> *arr*
#2A((0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))

We can displace another array onto *arr*. To do that we need to supply two argument :displaced-to and :displaced-index-offset, the first argument is the source array and the second is the first element of the source array at which our newly defined array starts. When we modify the new array, then in reality the *arr* is modified.

CL-USER> (defparameter *displaced*
           (make-array* '(4 4) :displaced-to *arr*
                               :displaced-index-offset '(4 4)))
*DISPLACED*
CL-USER> *displaced*
#2A((0 0 0 0) (0 0 0 0) (0 0 0 0) (0 0 0 0))

CL-USER> (loop for i from 0 below 4
               do (loop for j from 0 below 4
                        do (setf (aref *displaced* i j) 1)))
NIL
CL-USER> *displaced*
#2A((1 1 1 1) (1 1 1 1) (1 1 1 1) (1 1 1 1))
CL-USER> *arr*
#2A((0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1)
    (1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))

Had *displaced* been a conformally displaced array, the result would be far more intuitive, namely:

CL-USER> *arr*
#2A((0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0)
    (0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0)
    (0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0)
    (0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))

We can't portably extend existing array class, so we'll introduce a wrapper. Create a file cd-array.lisp and add it to the system definition (vconsole will depend on it). The class array* will have three slots: start, fillp and array, which are counterparts of displaced-index-offset, fill-pointer and displaced-to. The array dimensions are determined by start and fillp.

(defclass array* ()
  ((array :initarg :array :accessor %array)
   (start :initarg :start :accessor %start)
   (fillp :initarg :fillp :accessor %fillp)))

The class constructor will mimic the actual constructor for arrays and will be named make-array*.

;;; Function works like MAKE-ARRAY except that it allows FILL-POINTER
;;; and DISPLACED-INDEX-OFFSET to be lists. In that case it creates a
;;; wrapper instance which is conformally displaced onto the array.
;;; When the fill pointer is specified, but no DISPLACED-TO, then we
;;; create an array of the requested size. Effective start and fillp
;;; are stored (that is valid subscripts of the destination array).
(defun make-array* (dimensions &rest args
                    &key
                      (element-type t)
                      initial-element
                      initial-contents
                      adjustable
                      fill-pointer
                      displaced-to
                      displaced-index-offset)
  (declare (ignore element-type adjustable))
  (when (and (atom displaced-index-offset)
             (atom fill-pointer)
             (not (typep displaced-to 'array*)))
    (return-from make-array*
      (apply #'cl:make-array dimensions args)))
  (cond ((and (not displaced-to) displaced-index-offset)
         (error "Can't specify ~s without ~s."
                :displaced-index-offset :displaced-to))
        ((and displaced-to (or initial-element initial-contents))
         (error "~s and ~s are mutually exclusive with ~s."
                :initial-element :initial-contents :displaced-to))
        ((and (consp fill-pointer)
              (/= (length fill-pointer) (length dimensions)))
         (error "~s must have the same length as DIMENSIONS."
                :fill-pointer))
        ((and (consp displaced-index-offset)
              (/= (length displaced-index-offset) (length dimensions)))
         (error "~s must have the same length as DIMENSIONS."
                :displaced-index-offset)))
  (let* ((rank (length dimensions))
         (first (make-list rank :initial-element 0)))
    (when (null displaced-to)
      ;; implies that D-I-O is NIL and that F-P is CONS
      (remf args :fill-pointer)
      (return-from make-array*
        (make-instance 'array*
                       :array (apply #'cl:make-array dimensions args)
                       :start first
                       :fillp fill-pointer)))
    ;; Correct the FILL-POINTER and the DISPLACED-INDEX-OFFSET. Both
    ;; should be expressed in the destination array indexes.
    (cond ((and (atom fill-pointer)
                (atom displaced-index-offset))
           (setf displaced-index-offset first)
           (setf fill-pointer dimensions))
          ((atom fill-pointer)
           (setf fill-pointer (mapcar #'+ displaced-index-offset dimensions)))
          ((atom displaced-index-offset)
           (setf displaced-index-offset (mapcar #'- fill-pointer dimensions)))
          (t
           (setf fill-pointer (mapcar #'+ displaced-index-offset fill-pointer))))
    ;; Assert the indice correctness.
    (if (every #'<=
               first
               displaced-index-offset
               fill-pointer
               (mapcar #'+ displaced-index-offset dimensions)
               (array-dimensions* displaced-to))
        (make-instance 'array*
                       :array displaced-to
                       :start displaced-index-offset
                       :fillp fill-pointer)
        (error "Invalid FILL-POINTER or DISPLACED-INDEX-OFFSET specification."))))

We still have a few essentials operator to implement. Each of them will accept both cl:array and our own class as an argument. For instance array-dimensions* looks like this:

(defun array-dimensions* (array)
  (etypecase array
    (cl:array
     (cl:array-dimensions array))
    (array*
     (mapcar #'- (%fillp array) (%start array)))))

Then we have a very important functions, that is aref* and its setf variant. Both use the function get-real-subscripts.

(defun get-real-subscripts (array &rest subscripts)
  (loop for sub in subscripts
        for off in (%start array)
        for flp in (%fillp array)
        for ind = (+ sub off)
        if (and (>= ind off) (< ind flp))
          collect ind into subs
        else
          do (error "Invalid index.")
        finally
           (return subs)))

(defun aref* (array &rest subscripts)
  (etypecase array
    (cl:array
     (apply #'cl:aref array subscripts))
    (array*
     (apply #'cl:aref
            (%array array)
            (apply #'get-real-subscripts array subscripts)))))

(defun (setf aref*) (new-value array &rest subscripts)
  (etypecase array
    (cl:array
     (apply #'(setf cl:aref) new-value array subscripts))
    (array*
     (apply #'(setf aref*)
            new-value
            (%array array)
            (apply #'get-real-subscripts array subscripts)))))

The last function we will implement is adjust-array*. For real array we will call the function cl:adjust-array, and for conformally displaced arrays we will call make-array* with supplied arguments and simply copy slots from the result.

(defun adjust-array* (array dimensions &rest args
                      &key
                        element-type
                        initial-element
                        initial-contents
                        fill-pointer
                        displaced-to
                        displaced-index-offset)
  (declare (ignore element-type initial-element initial-contents)
           (ignore fill-pointer displaced-to displaced-index-offset))
  (etypecase array
    (cl:array
     (apply #'adjust-array array dimensions args))
    (array*
     (let ((arr (apply #'make-array* dimensions args)))
       (setf (%array array) (%array arr)
             (%start array) (%start arr)
             (%fillp array) (%fillp arr)))
     array)))

Our implementation is not optimized and is slower than direct array access. Moreover array* instances are not instances of the system class cl:array (so we didn't bother to implement all array operators, like array-displacement). Native implementation for instance could create a hash function which translates a row-major-index from the displaced array to its displaced-to array's row-major-index. The hash function would be recreated when the displaced-to array is adjusted. Since it is only a sketch, we'll settle on what we have now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment