AoC Benchmarks

aoc2021-day03b Common Lisp/SBCL #3 program

source code


;; SPDX-License-Identifier: LGPL-3.0-or-later
;; Copyright (C) 2021 Massimo Zaniboni <mzan@dokmelody.org>

(ql:quickload :trivia)     ;; common macro and functions and optimal pattern matching
(ql:quickload :alexandria) ;; common CL extensions
(ql:quickload :trivial-types)  ;; common types
(ql:quickload :defstar)    ;; add type annotations
(ql:quickload :str)        ;; Common string manipulation functions
(ql:quickload :parse-float)
(ql:quickload :iterate)
(ql:quickload :let-plus)          ;; extend "let"
(ql:quickload :array-operations)  ;; rich management of arrays
(ql:quickload :group-by)
(ql:quickload :cffi)
(ql:quickload :bit-smasher)
(ql:quickload :bordeaux-threads)
(ql:quickload :serapeum)
(ql:quickload :check-it)
(ql:quickload :parachute)

(defpackage :main
  (:import-from :alexandria)
  (:import-from :trivial-types :proper-list :tuple)
  (:use :cl :defstar :trivia :parse-float :iterate :let-plus :cffi)
  (:import-from :parachute :define-test :is :test :fail)
  (:import-from :group-by :group-by)
  (:export main))

(in-package :main)

;; ## Design
;;
;; The slowest part in the original CL algo was reading the file and parsing numbers.
;; So a very mundane task.
;; The initialization of Judy array was fast enough and the calculation of results was immediate,
;; because there is few data to retrieve from the Judy array, i.e. the bulk of the work is done from
;; the Judy array C data structure.
;;
;; The code is redesigned in this way:
;; * there is a thread per core
;; * each thread read from the file, and update a local Judy array
;; * final queries on Judy arrays, for calculating the result are done in sequence,
;;   because they are few

;; ## Initial definitions

(declaim (type fixnum +read-buff-size+))
(defconstant +read-buff-size+ (ash 1 16))

(declaim (type fixnum +initial-bits+))
(defconstant +initial-bits+ 26)

(deftype ubyte8 () '(unsigned-byte 8))

;; ## Import Judy array data structure from C

(define-foreign-library libJudy
  (:unix "libJudy.so")
  (t (:default "libJudy")))

(use-foreign-library libJudy)

(defctype judy-tree! (:pointer :pointer))

(defctype judy-tree :pointer)

(defctype judy-value! (:pointer :ulong))

(defcfun "JudyLIns" :pointer (tree judy-tree!) (index :ulong) (error :pointer))

(defcfun "JudyLCount" :ulong (tree judy-tree) (index1 :ulong) (index2 :ulong) (error :pointer))

(defcfun "JudyLFirst" :pointer (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "JudyLLast" :pointer (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "JudyLPrev" :pointer (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "JudyLNext" :pointer (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "JudyLGet" :pointer (tree judy-tree) (index :ulong) (error :pointer))

(defcfun "Judy1Set" :int (tree judy-tree!) (index :ulong) (error :pointer))

(defcfun "Judy1First" :int (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "Judy1Last" :int (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "Judy1Count" :ulong (tree judy-tree) (index1 :ulong) (index2 :ulong) (error :pointer))

(defcfun "Judy1Prev" :int (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "Judy1Next" :int (tree judy-tree) (index! judy-value!) (error :pointer))

(defcfun "Judy1Test" :int (tree judy-tree) (index :ulong) (error :pointer))

(defmacro judy-l-ins-ptr (tree! index)
  `(foreign-funcall "JudyLIns" judy-tree! ,tree! :ulong ,index :pointer (null-pointer) (:pointer :pointer)))

(defmacro judy-l-ins-ulong (tree! index)
  `(foreign-funcall "JudyLIns" judy-tree! ,tree! :ulong ,index :pointer (null-pointer) (:pointer :ulong)))

(declaim (inline judy-l-ins-ulong!))
(defun judy-l-ins-ulong! (tree! index value)
  (let ((ptr (judy-l-ins-ulong tree! index)))
  (setf (mem-ref ptr :ulong) value)))

(declaim (inline judy-l-ins-ptr!))
(defun judy-l-ins-ptr! (tree! index value-ptr)
  (let ((ptr (judy-l-ins-ptr tree! index)))
  (setf (mem-ref ptr :pointer) value-ptr)))

(defmacro judy-l-first-ptr (tree index!)
  `(foreign-funcall "JudyLFirst" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :pointer))

(defmacro judy-l-next-ptr (tree index!)
  `(foreign-funcall "JudyLNext" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :pointer))

(defmacro judy-l-prev-ptr (tree index!)
  `(foreign-funcall "JudyLPrev" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :pointer))

(defmacro judy-l-last-ptr (tree index!)
  `(foreign-funcall "JudyLLast" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :pointer))

(defmacro judy-l-get-ptr (tree index)
  `(foreign-funcall "JudyLGet" judy-tree ,tree :ulong ,index :pointer (null-pointer) :pointer))

(defmacro judy-1-set! (tree! index)
  `(foreign-funcall "Judy1Set" judy-tree! ,tree! :ulong ,index :pointer (null-pointer) :int))

(defmacro judy-1-first (tree index!)
  `(foreign-funcall "Judy1First" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :int))

(defmacro judy-1-last (tree index!)
  `(foreign-funcall "Judy1Last" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :int))

(defmacro judy-1-next (tree index!)
  `(foreign-funcall "Judy1Next" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :int))

(defmacro judy-1-prev (tree index!)
  `(foreign-funcall "Judy1Prev" judy-tree ,tree judy-value! ,index! :pointer (null-pointer) :int))

(defmacro judy-1-test (tree index)
  `(foreign-funcall "Judy1Test" judy-tree ,tree :ulong ,index :pointer (null-pointer) :int))

(defmacro judy-1-count (tree index1 index2)
  `(foreign-funcall
               "Judy1Count"
               judy-tree ,tree
               :ulong ,index1
               :ulong ,index2
               :pointer (null-pointer)
               :ulong))

(defmacro judy-l-count (tree index1 index2)
  `(foreign-funcall
               "JudyLCount"
               judy-tree ,tree
               :ulong ,index1
               :ulong ,index2
               :pointer (null-pointer)
               :ulong))


(declaim (inline last-pass?))
(defun last-pass? (pass# pass)
  (= pass (- pass# 1)))

(defun judy-1-scan (s tree indent)
  "Show the content of a Judy 1 array."

  (iter (with index! = (foreign-alloc :ulong :initial-element 0))
        (for continue? first (judy-1-first tree index!) then (judy-1-next tree index!))
        (while (= continue? 1))
        (after-each
          (iter (for i from 0 to indent)
                (after-each (format s "..")))
          (format s "~2R~%" (mem-ref index! :ulong)))))

(defun judy-l-scan (s tree pass# pass)
  "Show the content of nested Judy arrays."

  (iter (with is-last-pass? = (last-pass? pass# pass))
        (with is-penultimate-pass? = (last-pass? pass# (+ pass 1)))
        (with index! = (foreign-alloc :ulong :initial-element 0))
        (for c from 0)
        (for ptr first (judy-l-first-ptr tree index!) then (judy-l-next-ptr tree index!))
        (until (null-pointer-p ptr))
        (after-each
          (iter (for i from 0 to pass)
                (after-each (format s "..")))
          (format s "element ~a: ~2R~%" c (mem-ref index! :ulong))
          (unless is-last-pass?
            (if is-penultimate-pass?
                (judy-1-scan s (mem-ref ptr :pointer) (+ pass 1))
                (judy-l-scan s (mem-ref ptr :pointer) pass# (+ pass 1)))))))

(defun judy-scan (s tree pass# pass)
  (if (last-pass? pass# pass)
      (judy-1-scan s tree pass)
      (judy-l-scan s tree pass# pass)))


;; ## Day 03b exsercise

(deftype read-buffer () '(simple-array ubyte8 1))

(deftype bitmask () '(simple-array bit 1))

(defconstant +max-bits+ (- (max (integer-length most-positive-fixnum) (integer-length most-negative-fixnum)) 1)
  "After these bits, native numbers are not any more nice inside CL run-time, because they will be big numbers.
I subtract 1 bit for avoiding problems with signed fixnum, also if probably not necessary.")

(defconstant +group-by+ +max-bits+)

(deftype judy-value ()
  "A judy-value as a CL friendly type."
  `(unsigned-byte ,+group-by+))

(defconstant judy-value-min (coerce 0 'judy-value))

(defconstant judy-value-max
  (iter (for i from 0 below +max-bits+)
        (for bm first (coerce 1 'judy-value) then (coerce (ash bm 1) 'judy-value))
        (for r first (coerce 1 'judy-value) then (coerce (boole boole-ior r bm) 'judy-value))
        (finally (return r))))

(declaim (type judy-value judy-value-min judy-value-max))

(deftype small-counter () '(signed-byte 16))

(defun* (reversed-first-line-bits->buffer -> read-buffer) ((bits# small-counter) (rev-bits (proper-list ubyte8)))
  (make-array
     bits#
     :adjustable nil
     :element-type 'ubyte8
     :initial-contents (reverse rev-bits)))

(defun* (parse-each-byte-of-the-first-line -> (values small-counter read-buffer)) ((s stream))
  "Parse slowly the first line, for detecting the number of bits."
  (iter (for v in-stream s :using #'read-byte)
        (until (or (null v) (= 10 v)))
        (counting v into c)
        (collecting v into vs at beginning)
        (finally (return (values c (reversed-first-line-bits->buffer c vs))))))

(declaim (inline insert-bits-into-judy!))
(defun* (insert-bits-into-judy! -> fixnum)
          ((root-tree! SB-SYS:SYSTEM-AREA-POINTER)
           (pass# small-counter)
           (last-pass-bits small-counter)
           (buffer read-buffer)
           (i fixnum))
  "Insert the bits in a chain of slots of Judy arrays:

   - bits are grouped in `+group-by+' bits, because a single index can be insufficient;
   - the last Judy is a Judy Set, having the bits in the index;
   - the penultimate Judy array put the first bits in the inedx, and the value is a pointer to the Judy set with other part of te bits;
   - and so on...

   Return the next position of the buffer to read.
   "

  (declare (optimize (speed 0) (debug 3) (safety 3)))

  (iter (with slot! = root-tree!)
        (for pass from 0 below pass#)
        (declare (dynamic-extent pass))
        (after-each
         (iter  (with index = (coerce 0 'judy-value))
                (declare (judy-value index))
                (with last-pass? = (= pass (- pass# 1)))
                (with bits-in-pass = (if last-pass? last-pass-bits +group-by+))
                (for j from (- bits-in-pass 1) downto 0)
                (declare ((or null small-counter) j))
                (declare (dynamic-extent index last-pass? bits-in-pass j))
                (after-each
                 (when (= 49 (the ubyte8 (aref buffer i)))
                   (setf index (boole boole-ior index (coerce (ash 1 j) 'judy-value))))
                  (incf i))
                (finally
                   ; skip newline
                   (when last-pass?
                     (incf i))

                   ; NOTE: Judy has a very compact API, but it is rather obscure.
                   (cond
                      (last-pass?
                        (judy-1-set! slot! index)
                        (setf slot! (mem-ref root-tree! :pointer)))
                      (t (setf slot! (judy-l-ins-ptr slot! index)))))))
        (finally (return i))))

(defun* (day3b-thread -> SB-SYS:SYSTEM-AREA-POINTER)
    ((s stream)
     (lock-s sb-thread:mutex)
     (pass# small-counter)
     (last-pass-bits small-counter)
     (buffer-size fixnum))
  "Use bytes because managing *input-stream* as characters (but not normal stream files), make it too much slower (10x more slower!).
   Manage the input stream s as a shared resource.

   Add each index inside the Judy array.
   Decompose the long line in chunks of +group-by+ bit and store them in nested judy-trees.
   The last JudyTree of the sequence is a Judy1 set.
"

  (iter
     (with root-tree! = (foreign-alloc :pointer :initial-element (null-pointer)))
     (with buffer = (make-array buffer-size :adjustable nil :element-type 'ubyte8))
     (declare (read-buffer buffer))

     (for end = (bt:with-lock-held (lock-s) (read-sequence buffer s)))
     (declare (fixnum end))

     (until (zerop end))

     (after-each
       ; Process each line of the buffer.
       ; NOTE: requires that `buffer-size' is aligned with `cols#'

       (iter (with i = (coerce 0 'fixnum))
             (declare (fixnum i))
             (declare (dynamic-extent i))
             (while (< i end))
             (after-each
               (setf i (insert-bits-into-judy! root-tree! pass# last-pass-bits buffer i)))))

     (finally (return (mem-ref root-tree! :pointer)))))

(defun* (judy-tree-rating-pass -> judy-value)
          ((trees (proper-list SB-SYS:SYSTEM-AREA-POINTER))
           (bits-in-pass small-counter)
           (last-pass? boolean)
           (follow-max? boolean))

  "Count for every bit the elements inside the Judy arrays with 0 and 1 values,
   until a unique element is found.
   Work on a list of distinct judy trees, produced by distinct threads.
   Do not consider Judy arrays nested, so it returns only the index of current pass."

   (iter
     (with result = (coerce 0 'judy-value))
     ; the current selected result, completede from most significative bits to lower

     (with index1 = (coerce 0 'judy-value))
     ; the minimum element that can be selected (comprehensive)

     (with index2 = judy-value-max)
     ; the maximum element that can be selected (comprehensive)

     (with tot-n = 0)
     ; the total number of elements to select in each tree

     (initially
       (iter (for tree in-sequence trees)
             (after-each
               (let* ((n (if last-pass?
                          (judy-1-count tree index1 index2)
                          (judy-l-count tree index1 index2))))
                 (incf tot-n n)))))

     (for i from (- bits-in-pass 1) downto 0)
     (for bitmask-on-i = (coerce (ash 1 i) 'judy-value))
     ; Select each bit counting the elements with this bit set to 0 or 1,
     ; starting from most significative bit

     (after-each
      (assert (> tot-n 0))

        (setf result (boole boole-ior result bitmask-on-i))
        ; consider the next higher possible value in the bit

        (let* ((tot-0b 0)
               ; how many elements there are with this bit set to 0

               (tot-1b 0))

          ; Collect totals for each tree
          (iter (for tree in-sequence trees)
                (after-each
                  (incf tot-1b (if last-pass?
                                   (judy-1-count tree result index2)
                                   (judy-l-count tree result index2)))))

          (setf tot-0b (- tot-n tot-1b))

         ; Decide the bit to follow, according the total sums of 0 and 1 bits
          (let* ((empty-1bit? (zerop tot-1b))
                 (empty-0bit? (zerop tot-0b))
                 (take-1bit?  (cond
                                (empty-1bit? nil)
                                (empty-0bit? t)
                                (t (if follow-max?
                                       (>= tot-1b tot-0b)
                                       (< tot-1b tot-0b))))))

          (assert (not (and empty-1bit? empty-0bit?)))
          (cond
             (take-1bit?
              (setf tot-n tot-1b)
              (setf index1 result))
             (t
               ; take 0 bit, instead

               (setf tot-n tot-0b)

               (setf index2 (- result 1))
               ; the new bottom limit is a number with 1 in the rest of the bits.
               ; This is required because bottom limit is comprehensive, and not exclusive

               ; revert to 0 the bit, instead to 1
               (setf result (boole boole-xor result bitmask-on-i)))))))
     (finally (return result))))

(defun* (judy-tree-rating -> integer)
          ((trees (proper-list SB-SYS:SYSTEM-AREA-POINTER))
           (pass# small-counter)
           (bits# small-counter)
           (last-pass-bits small-counter)
           (follow-max? boolean))

  "Apply judy-tree-rating-pass to each nested judy-tree, i.e. each nested judy-tree is another pass
containing lesser significative bits, until last-pass is reached.

The returned integer can be bigger than judy-value, because it can be a multi-word value."

  (iter
     (with curr-trees = trees)
     (with result = 0)
     (with left-bits = bits#)
     (for pass from 0 below pass#)
     (for last-pass? = (= pass (- pass# 1)))
     (for bits-in-pass = (if last-pass? last-pass-bits +group-by+))
     (after-each
        (let* ((index (judy-tree-rating-pass curr-trees bits-in-pass last-pass? follow-max?)))
          (unless last-pass?
                  (setf curr-trees
                        (iter
                          (for curr-tree in-sequence curr-trees)
                          (for ptr = (judy-l-get-ptr curr-tree index))
                          (when (null-pointer-p ptr) (next-iteration))
                          (collecting (mem-ref ptr :pointer) at beginning))))

          (decf left-bits bits-in-pass)
          (incf result (ash index left-bits))))
     (finally (return result))))

(defun day3b-main-thread (s)
  (bt:start-multiprocessing)

  (multiple-value-bind (bits# first-line-bits)
    (parse-each-byte-of-the-first-line s)
    (declare (small-counter bits#))
    (let* ((cols# (+ bits# 1))
           (last-pass-bits (if (= bits# +group-by+) +group-by+ (mod bits# +group-by+)))
           (buffer-lines# (coerce (ceiling +read-buff-size+ cols#) 'fixnum))
           (buffer-size (* cols# buffer-lines#))
           (pass# (ceiling bits# +group-by+))

           (cores# (serapeum:count-cpus))

           (lock-s (bt:make-lock))
           (threads
             (iter (for i from 0 below cores#)
                   (collect
                       (bt:make-thread
                        (lambda ()
                          (day3b-thread s lock-s pass# last-pass-bits buffer-size)))
                    at beginning)))

           (judy-trees
             (remove-if #'null-pointer-p (map 'list #'bt:join-thread threads))))

          (declare (small-counter last-pass-bits))

          ; Add the first line to a judy tree
          (let ((car-tree! (foreign-alloc :pointer :initial-element (car judy-trees))))
            (insert-bits-into-judy! car-tree! pass# last-pass-bits first-line-bits 0)
            (setf judy-trees (cons (mem-ref car-tree! :pointer) (cdr judy-trees))))

          (let* ((oxygen (judy-tree-rating judy-trees pass# bits# last-pass-bits t))
                 (co2 (judy-tree-rating judy-trees pass# bits# last-pass-bits nil)))
             (values (* oxygen co2) oxygen co2)))))


(defun day3b-main-thread-fn (fn)
  (with-open-file (s fn :element-type 'ubyte8)
    (day3b-main-thread s)))

(defun main () (format t "~a~%" (day3b-main-thread *standard-input*)))
    

notes, command-line, and program output

NOTES:
Linux


Mon, 24 Jan 2022 01:08:18 GMT

MAKE:
sbcl --disable-debugger --load "aoc2021_day03b.lisp-3.lisp" --eval "(sb-ext:save-lisp-and-die \"app_lisp\" :executable t  :toplevel #'main:main)"
This is SBCL 2.1.11, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
To load "trivia":
  Load 1 ASDF system:
    trivia
; Loading "trivia"

To load "alexandria":
  Load 1 ASDF system:
    alexandria
; Loading "alexandria"

To load "trivial-types":
  Load 1 ASDF system:
    trivial-types
; Loading "trivial-types"

To load "defstar":
  Load 1 ASDF system:
    defstar
; Loading "defstar"

To load "str":
  Load 1 ASDF system:
    str
; Loading "str"
...
To load "parse-float":
  Load 1 ASDF system:
    parse-float
; Loading "parse-float"

To load "iterate":
  Load 1 ASDF system:
    iterate
; Loading "iterate"

To load "let-plus":
  Load 1 ASDF system:
    let-plus
; Loading "let-plus"

To load "array-operations":
  Load 1 ASDF system:
    array-operations
; Loading "array-operations"

To load "group-by":
  Load 2 ASDF systems:
    alexandria iterate
  Install 1 Quicklisp release:
    group-by
; Fetching #<URL "http://beta.quicklisp.org/archive/group-by/2014-02-11/group-by-20140211-git.tgz">
; 8.86KB
==================================================
9,071 bytes in 0.00 seconds (0.00KB/sec)
; Loading "group-by"
[package group-by]..
To load "cffi":
  Load 1 ASDF system:
    cffi
; Loading "cffi"
.
To load "bit-smasher":
  Load 1 ASDF system:
    bit-smasher
; Loading "bit-smasher"

To load "bordeaux-threads":
  Load 2 ASDF systems:
    alexandria asdf
  Install 1 Quicklisp release:
    bordeaux-threads
; Fetching #<URL "http://beta.quicklisp.org/archive/bordeaux-threads/2020-06-10/bordeaux-threads-v0.8.8.tgz">
; 23.15KB
==================================================
23,709 bytes in 0.02 seconds (1447.08KB/sec)
; Loading "bordeaux-threads"
[package bordeaux-threads]....
To load "serapeum":
  Load 8 ASDF systems:
    alexandria asdf babel bordeaux-threads
    introspect-environment trivia trivial-cltl2 uiop
  Install 9 Quicklisp releases:
    global-vars parse-declarations parse-number serapeum
    split-sequence string-case trivial-file-size
    trivial-garbage trivial-macroexpand-all
; Fetching #<URL "http://beta.quicklisp.org/archive/trivial-macroexpand-all/2017-10-23/trivial-macroexpand-all-20171023-git.tgz">
; 1.92KB
==================================================
1,968 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/trivial-garbage/2021-12-30/trivial-garbage-20211230-git.tgz">
; 10.74KB
==================================================
10,996 bytes in 0.00 seconds (2684.57KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/trivial-file-size/2020-04-27/trivial-file-size-20200427-git.tgz">
; 3.13KB
==================================================
3,208 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/string-case/2018-07-11/string-case-20180711-git.tgz">
; 8.87KB
==================================================
9,081 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/split-sequence/2021-05-31/split-sequence-v2.0.1.tgz">
; 11.43KB
==================================================
11,705 bytes in 0.00 seconds (2857.67KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/parse-number/2018-02-28/parse-number-v1.7.tgz">
; 5.58KB
==================================================
5,715 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/parse-declarations/2010-10-06/parse-declarations-20101006-darcs.tgz">
; 35.80KB
==================================================
36,664 bytes in 0.03 seconds (1118.86KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/global-vars/2014-11-06/global-vars-20141106-git.tgz">
; 3.50KB
==================================================
3,581 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/serapeum/2021-12-30/serapeum-20211230-git.tgz">
; 219.43KB
==================================================
224,698 bytes in 0.10 seconds (2194.27KB/sec)
; Loading "serapeum"
[package split-sequence]..........................
[package string-case].............................
[package org.mapcar.parse-number].................
[package trivial-garbage].........................
[package tcr.parse-declarations-1.0]..............
[package global-vars].............................
[package trivial-file-size].......................
[package trivial-macroexpand-all].................
[package serapeum.sum]............................
[package serapeum]................................
[package serapeum-user]...........................
[package serapeum.unlocked].......................
[package serapeum/op].............................
..................................................
..................................................
[package serapeum/vector=]........................
[package serapeum/mop]............................
[package serapeum/internal-definitions]...........
..................................................
[package serapeum/dispatch-case]..................
[package serapeum/generalized-arrays].............
[package serapeum/contrib/hooks]....
To load "check-it":
  Load 2 ASDF systems:
    alexandria closer-mop
  Install 2 Quicklisp releases:
    check-it optima
; Fetching #<URL "http://beta.quicklisp.org/archive/optima/2015-07-09/optima-20150709-git.tgz">
; 19.87KB
==================================================
20,345 bytes in 0.01 seconds (2483.52KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/check-it/2015-07-09/check-it-20150709-git.tgz">
; 19.52KB
==================================================
19,988 bytes in 0.02 seconds (975.98KB/sec)
; Loading "check-it"
[package optima.core].............................
[package optima]..................................
[package optima.extra]............................
[package check-it]............
To load "parachute":
  Load 1 ASDF system:
    asdf
  Install 4 Quicklisp releases:
    documentation-utils form-fiddle parachute trivial-indent
; Fetching #<URL "http://beta.quicklisp.org/archive/form-fiddle/2019-07-10/form-fiddle-20190710-git.tgz">
; 5.50KB
==================================================
5,635 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/trivial-indent/2021-05-31/trivial-indent-20210531-git.tgz">
; 3.48KB
==================================================
3,564 bytes in 0.00 seconds (0.00KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/documentation-utils/2019-07-10/documentation-utils-20190710-git.tgz">
; 8.70KB
==================================================
8,913 bytes in 0.00 seconds (2176.03KB/sec)
; Fetching #<URL "http://beta.quicklisp.org/archive/parachute/2021-10-20/parachute-20211020-git.tgz">
; 52.49KB
==================================================
53,747 bytes in 0.04 seconds (1192.87KB/sec)
; Loading "parachute"
[package trivial-indent]..........................
[package documentation-utils].....................
[package form-fiddle].............................
[package parachute].................
[undoing binding stack and other enclosing state... done]
[performing final GC... done]
[defragmenting immobile space... (fin,inst,fdefn,code,sym)=1543+1158+22532+23073+26415... done]
[saving current Lisp image into app_lisp:
writing 0 bytes from the read-only space at 0x50000000
writing 736 bytes from the static space at 0x50100000
writing 56262656 bytes from the dynamic space at 0x1000000000
writing 2228224 bytes from the immobile space at 0x50200000
writing 15421440 bytes from the immobile space at 0x52a00000
done]

10.87s to complete and log all make actions

COMMAND LINE:
./app_lisp 0 < aoc2021_day03b-input100000.txt

PROGRAM OUTPUT:
356913958942791247617705918285570893096041618195840162127310