ClojureScript Const Var Inlining

June 28, 2017

The ClojureScript 1.9.655 release introduced ^:const Var inlining. This is an exciting new feature—upgrade your compiler and give it a spin! This post will provide some detail on this new feature, covering some of its more subtler aspects.




Review and Definition

Previously, adding :const metadata to a Var, as in

(def ^:const foo 3)

would have two effects in ClojureScript:

  • It prevents re-definition within the same namespace.
  • It allows case test constants which are symbols resolving to ^:const Vars to represent their values.

Now, with ClojureScript 1.9.655 and later, a new behavior is introduced:

  • Compile-time static edn values are inlined.

This means, for example, with the Var foo as defined above,

(+ foo 12)

effectively acts as if converted to

(+ 3 12)

What is a compile-time static edn value? It can be defined recursively as a value produce by:

  • A literal which is any of nil, a Boolean, a number, a character, a string, a keyword, a symbol, an #inst, or a #uuid.
  • A (potentially empty) persistent collection literal (any of set, map, vector, or list) containing compile-time static edn values.

As expected, quoted versions of the above are fine. This is essential for creating symbol and list constants, and also allows you to easily create, say, a set collection constant containing symbols.

Examples include 1, "foo", and {:a 'sym}.

If you define a ^:const Var initialized with something that is not a compile-time static edn value, then the value will not be inlined (but the no re-definition rule will still apply).

Also note that, while a value could effectively be considered a compile-time constant, this

(def ^:const foo (str "bar" "baz"))

won't be inlined owing to the compilation and evaluation model. The initializer will evaluated in the target JavaScript environment; not at compile time.

While it is tempting to consider having the compiler calculate initializer values for pure functions (producing the constant "barbaz" above), this would fail to work properly for hosty values: (+ 16r1FFFFFFFFFFFFF 2) would produce two different values if evaluated at compile-time vs. runtime for non-self-hosted ClojureScript.

Performance and Behavior

The primary benefit of using ^:const is avoiding Var indirection and having constants directly embedded in the emitted JavaScript.

You can look at the JavaScript produced in a REPL by using the :repl-verbose REPL option. (More: See JS in CLJS REPL)

The JavaScript produced for (+ foo 12), where foo was defined as above is:

((3) + (12))

This can clearly be a win for “small” constants, leading to more compact and direct code.

On the other hand, if you have something like a map value, depending on access patterns, it might be more efficient to not define it as a constant. For example

(def ^:const m {:a 1 :b 2 :c 3})

(defn lookup [] (:a m))

(simple-benchmark [] (lookup) 1e7)

yields a timing of 1629 milliseconds on my machine, but if I leave the ^:const off, I get a timing of 480 milliseconds.

If you look at the JavaScript produced, you can see that the difference boils down to either constructing a new map value each time lookup is called (the inlined constant map value), or referring to a single instance of the map via a JavaScript variable. (In effect, leaving ^:const off, in this case, leads to a result that is somewhat akin what occurs when you use the :optimize-constants compiler option for keywords and symbols.)

If you'd like to have a ^:const Var not be inlined, you can defeat inlining by simply wrapping the value with a call to identity. For the example above: (def ^:const m (identity {:a 1 :b 2 :c 3})).

What about case? With the new compiler, test constants which are symbols resolving to ^:const Vars will be inlined with their values, instead of representing their values.

For example, this case at the beginning of this post compiles down to JavaScript that looks like this:

switch (x) {
  case 3:
    return 1;
    break;
  case 4:
    return 2;
    break;
  case 7:
    return 3;
    break;
  case "hi":
    return 4;
    break;
  default:
    return new cljs.core.Keyword(null,"no-match",
     "no-match",(568148921));
}

Notice that the JavaScript now has case 3 and case 4 instead of case foo.bar.n and case foo.bar.m.

Identity

Another difference in behavior that can be observed is with the use of identical? in the case where inlined constants involve allocation. This code

(def ^:const kw :foo)

(identical? kw kw)

now yields false, where it would yield true without ^:const. But, of course, since keywords and symbols are not interned, keyword-identical? and symbol-identical? can be used for efficient comparison, even in the case of different object instances; keyword-identical? returns true for the example above.

Caching

One final caution with respect to inlining behavior: If you have a namespace that uses ^:const Vars from another namespace, and you change the value of such a constant, you my need to ensure that the consuming namespace is re-compiled so that the updated constant value is re-inlined.

For example, let's say foo/core.cljs contains:

(ns foo.core)

(def ^:const a 1)
(prn a)

and bar/core.cljs contains:

(ns bar.core
  (:require foo.core))

(def b foo.core/a)
(prn b)

If at the REPL, you (require 'bar.core) you will see 1 printed twice. If you then quit the REPL, change foo.core/a to be initialized with 2 and then go back into the REPL, you will see 2 printed, followed by 1.

You could fix this by either deleting your cache, or by touching bar/core.cljs and re-doing the require.

Summary

For most existing code making use of :^const, you need do nothing—things will just run faster. I hope the above gives further insight into how this feature works. I think it is exciting—you should give it a try!

Tags: ClojureScript