Simple experiment to test the effects of different techniques and options on application start up time
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/
To decrease class size and make classloading faster, meta can be elided.
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.
- 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
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 |
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
(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]))))
(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))
#!/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