RSS Feed

Lisp Project of the Day


You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.


Tests 😀
CI 😀

This is a template engine which is able to compile templates into the fast code.

Here is a quick example:

POFTHEDAY> (zenekindarl:compile-template-string
            :string "Hello {{var name}}!")

POFTHEDAY> (funcall * :name "Bob")
"Hello Bob!"

Interesting fact – it is named after a race of ancient people.

When searching this information, I've also found this page about the Dexador which is a dragon of Zenekindarl people and also a CL HTTP library :)

Documentation says, this library is faster than Python's Jinja2 which known for it's ability to compile templates into the bytecode.

Let's make our own comparison! We'll do the test more complex and extend it to the cl-who. Later I'll add to this comparison other template engines during the writing their review.

Let's start with the baseline test for Jinja2:

Python 3.7.7 (default, Mar 10 2020, 15:43:33)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.8.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from jinja2 import Template

In [2]: template = Template("""
   ...: <h1>{{ title }}</h1>
   ...: <ul>
   ...: {% for item in items %}
   ...:   <li>{{ item }}</li>
   ...: {% endfor %}
   ...: </ul>
   ...: """)

In [3]: %timeit template.render(title='Foo Bar', items=['One', 'Two', 'Tree'])
6.18 µs ± 37 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

As you see, each call to the render takes 6.18 microseconds.

Now we'll implement it in zenekindarl:

POFTHEDAY> (let ((tmpl (zenekindarl:compile-template-string
                        :string "
<h1>{{ var title }}</h1>
{{ loop items as item }}
  <li>{{ var item }}</li>
{{ endloop }}
              (loop repeat 1000000
                    do (funcall tmpl
                                :title "Foo Bar"
                                :items '("One" "Two" "Three")))))
Evaluation took:
  1.538 seconds of real time
  1.546164 seconds of total run time (1.525024 user, 0.021140 system)
  [ Run times consist of 0.173 seconds GC time, and 1.374 seconds non-GC time. ]
  100.52% CPU
  3,395,746,202 processor cycles
  911,998,848 bytes consed

Hmm, with zenekindarl it takes 1.54 microseconds. This is only 4 times faster, not 25 times like promised by the documentation.

Now let's compare it with the hand-written cl-who code:

POFTHEDAY> (defun render (title items)
             (cl-who:with-html-output-to-string (s)
               (:h1 (cl-who:esc title)
                     (loop for item in items
                           do (cl-who:htm
                               (:li (cl-who:esc item))))))))

            (loop repeat 1000000
                  do (render "Foo Bar"
                             '("One" "Two" "Three"))))
Evaluation took:
  1.644 seconds of real time
  1.646934 seconds of total run time (1.619237 user, 0.027697 system)
  [ Run times consist of 0.130 seconds GC time, and 1.517 seconds non-GC time. ]
  100.18% CPU
  3,630,166,196 processor cycles
  687,990,384 bytes consed

Well, we've received almost the same performance - 1.64 microseconds!

This library seems interesting to me. But lack of documentation does not allow to understand its limits. Probably it is very extensible. Hope, @blackenedgold will write more documentation or a tutorial someday.

Performance results

This code will render us a chart:

;; Colors are taken from
POFTHEDAY> (let* ((data '(("Jinja2 (Python)" 6.18 "Tomato")
                          ("zenekindarl" 1.54 "MediumSeaGreen")
                          ("cl-who" 1.64 "RoyalBlue")
                          ("spinneret (pretty)" 4.94 "SandyBrown")
                          ("spinneret" 1.08 "SandyBrown")
                          ("cl-mustache" 5.21 "DeepPink")
                          ("print-html" 3.26 "GreenYellow")
                          ("djula" 4.48 "Gold")
                          ("cl-emb" 1.44 "MediumSlateBlue")
                          ("eco" 2.14 "Crimson")
                          ("handwritten" 0.93 "DarkOrange")))
                  (data (sort data #'> :key #'second))
                  (baseline (second (first data)))
                  (base-width 600))
             (cl-who:with-html-output-to-string (s)
               (loop for (name value color) in data
                     for width = (* (/ base-width baseline)
                     for style = (format nil "width: ~Apx; background-color: ~A; color: white; padding: 0.5em; display: inline-block;"
                     do (cl-who:htm
                         (:div :style "margin-top: 1em;"
                          (:span :style style
                                      (cl-who:esc name))
                          " – "
                          (:span (cl-who:esc (format nil "~A µs" value))))))))

This test was conducted on Macbook 2,2 GHz 6-Core Intel Core i7, with Python 3.7.7 and SBCL 2.0.8:

Shorter bar is better - it shows the library is faster:

Jinja2 (Python)6.18 µs
cl-mustache5.21 µs
spinneret (pretty)4.94 µs
djula4.48 µs
print-html3.26 µs
eco2.14 µs
cl-who1.64 µs
zenekindarl1.54 µs
cl-emb1.44 µs
spinneret1.08 µs
handwritten0.93 µs

Update from 2020-09-14

Performance results for Spinneret were added to the chart. Code is in the post #0189.

Update from 2020-09-15

Performance results for cl-mustache and print-html were added to the chart. Code is in the post #0190 and post #0049.

Update from 2020-09-16

Added performance results for handwritten HTML generator and the djula library. Also added the result of calling Spinneret without pretty printing.

Update from 2020-09-17

Added performance results for cl-emb.

Update from 2020-09-18

Added performance results for eco.

Brought to you by 40Ants under Creative Commons License