Performance is weird

I have a lot of performance data about tartrazine since it’s a project with a very deterministic output on which I have done extensive performance work, and it’s very easy to benchmark.

I wanted to get a historic perf benchmark for the different releases, which all include static alpine-musl binaries.

Well, it was weird.

The blue lines are benchmarks of each release built on my machine. The red lines are benchmarks of each release using the static binaries from the release (lower is better).

See that performance loss? Static binaries got a lot slower at one point.

Of course this covers many months in time, so it could be many things:

  • Crystal version
  • Dependency versions
  • Alpine version

After lots of compiling and comparing …

  • In musl, static builds, crystal 1.21.0 is much slower than 1.20.1
  • In glibc, dynamic builds, 1.21.0 and 1.20.1 perform the same

After debugging, the performance variation can be attributed to … garbage collection.

  • In 1.21.0 + static + musl the heap size is “grow as you go”
  • If I set it to start at 16MB then it’s fastest of all compiler versions I tried
1.21.0 static build, sqlite3.h → html wall time
default GC settings 94.7 ms
GC_INITIAL_HEAP_SIZE=16M 49.2 ms
1.20.1 default (control) 54.5 ms

This is a 50% reduction in wall time, entirely from changing a parameter!!! :rofl:

  • Not the dependencies
  • Not the compiler version
  • Not the operating system

The heap size.

While this is probably not universally applicable (do you really care about a 40ms startup shave?) it was super fun to figure out!

Interesting investigation, but a few points are still unclear to me.

  • GC_INITIAL_HEAP_SIZE is provided by Boehm GC, not Crystal.
  • From a DeepWiki scan of the Crystal codebase — with some hallucination risk — Crystal does not seem to configure it and simply leaves it to Boehm GC.

So could this be due to a recent change in libgc, where different linked versions caused different initial allocation behavior?

Also, assuming the same libgc version, why would dynamic linking be faster than static linking when memory is allocated “as you go”?