Why
Simple experiment to test the effects of different techniques and options on application start up time
Class Data Sharing (CDS)
The goal of CDS is to reduce the startup time of the JVM by loading from a pre-processed archive of Java classes and JVM metadata that is used during the initialization process. https://dev.java/learn/jvm/cds-appcds/
Clojure compiler options
Meta elision
To decrease class size and make classloading faster, meta can be elided.
Direct linking
Direct linking can be used to replace this indirection with a direct static invocation of the function instead. This will result in faster var invocation. Additionally, the compiler can remove unused vars from class initialization and direct linking will make many more vars unused. Typically this results in smaller class sizes and faster startup times.
Technique
- Clone Sean Corfield's user manager example: https://github.com/seancorfield/usermanager-example
- Hack the uber opts to conditionally add direct linking and elide meta compiler options
- Run and measure with hyperfine
- Tested with openjdk 17 on AMD CPU
Results
The table below displays hyperfine results for mean application start up time with different options enabled and speed up relative to the baseline
cds | direct | elide | mean ms | stddev ms | speedup% |
---|---|---|---|---|---|
false | false | false | 1431 | 18 | 0 |
false | false | true | 1422 | 33 | .63 |
false | true | false | 1388 | 18 | 3 |
false | true | true | 1379 | 17 | 3.6 |
true | false | false | 992.9 | 16.4 | 30 |
true | false | true | 992.4 | 20.1 | 30 |
true | true | false | 972.6 | 12.5 | 32 |
true | true | true | 959.4 | 12.3 | 33 |
Discussion
CDS has significant flat speed up effect. Consider using it if you have to shave off your start up time
For big applications direct linking and meta elision have small effect on start up time.
They have other performance benefits, especially direct linking, on application stready state performance.
The effort required to enjoy these improvements was very little
Implementation
hack uber opts
(defn- uber-opts [{:keys [elide direct] :as opts}]
(-> opts
(assoc
:lib lib :main main
:uber-file (format "target/%s-standalone.jar" lib)
:basis (b/create-basis {})
:class-dir class-dir
:src-dirs ["src"])
(cond->
direct (assoc-in [:compile-opts :direct-linking] true)
elide (assoc-in [:compile-opts :elide-meta] [:doc :file :line]))))
hack main
(defn -main
[& [port]]
(let [port (or port (get (System/getenv) "PORT" 8080))
port (cond-> port (string? port) Integer/parseInt)]
(println "Starting up on port" port)
;; start the web server and application:
(-> (component/start (new-system port false))
;; then put it into the atom so we can get at it from a REPL
;; connected to this application:
(->> (reset! repl-system))
;; then wait "forever" on the promise created:
#_#_#_:web-server :shutdown deref))
(System/exit 0))
run script
#!/usr/bin/env sh
for direct in true false;
do
for elide in true false;
do
echo direct $direct elide $elide
clojure -T:build ci :elide $elide :direct $direct && \
java \
-XX:ArchiveClassesAtExit=archive.jsa \
-jar target/usermanager/example-standalone.jar && \
hyperfine 'java -XX:SharedArchiveFile=archive.jsa -jar target/usermanager/example-standalone.jar' && \
hyperfine 'java -jar target/usermanager/example-standalone.jar'
done
done