CPS Performance

The Performance Cost of the CPS Interpreter

Every CPS-transformed expression allocates continuation objects and passes through the interpreter, and in a sandboxed script every call is also checked against the allow-list. This job sums integers in a loop, once in a CPS-transformed method and once in an identical @NonCPS one:

cps-speed: the same loop, interpreted and compiledPython
def cpsSum(int n) { long s = 0; for (int i = 0; i < n; i++) { s += i }; s }
@NonCPS
def plainSum(int n) { long s = 0; for (int i = 0; i < n; i++) { s += i }; s }
long t0 = System.currentTimeMillis(); long a = cpsSum(100000)
long t1 = System.currentTimeMillis(); long b = plainSum(100000)
long t2 = System.currentTimeMillis()
echo "CPS ${t1 - t0} ms, @NonCPS ${t2 - t1} ms, same result: ${a == b}"

It ran twice as the sandboxed job cps-speed and twice as cps-speed-trusted, the same script with the sandbox off and approved by an administrator. The machine is a shared 4-CPU host on which another writer's Kubernetes 5,150 cluster was busy, so compare ratios rather than milliseconds:

Output of 15
CPS 31800 ms, @NonCPS 25551 ms, same result: true
CPS 36775 ms, @NonCPS 32945 ms, same result: true
CPS 9969 ms, @NonCPS 27 ms, same result: true
CPS 9128 ms, @NonCPS 52 ms, same result: true

The first two lines are sandboxed: the per-call security checks dominate, and CPS adds only 12 to 24%. Without the sandbox, interpretation made the same loop 175 to 370 times slower. And this is time on the controller's CPU, shared by every running build. Jenkins 8,793 even records its own overhead in each build.xml: durable-demo's build 3 spent 0.99 s parsing, 1.04 s loading classes, 1.79 s running the program and 0.36 s saving it.

The rule the Jenkins documentation gives follows: a Jenkinsfile is glue. Parse JSON, compute versions and crunch results in scripts run by sh on the agent, at native speed and as durable tasks, and keep Groovy for deciding which steps run.