Hm ... ifTrue:ifFalse: sends are inlined whereas ifEmpty:ifNotEmpty: receives regular block closures, right? By the way, it's 5 % GC time for me only.
Monitoring BlockClosure allSubInstances size reveals that there are being created significantly more blocks in the image while running
[ [ [ #() ifEmpty: ['hallo'] ifNotEmpty: ['squeak'] ] repeat ] valueWithin: 10 seconds onTimeout: [] ] forkAt: 30.
Turns out that Object>>#value is faster (though less idiomatic):
[ #() isEmpty ifTrue: ['hallo'] ifFalse: ['squeak'] ] bench '52,900,000 per second. 18.9 nanoseconds per run. 0 % GC time.'
pushFullClosure: seems to create block instances eagerly (as opposed to Context instances).
Could a clever bytecode optimization share BlockClosure instances with the same/empty set of enclosed variables?
For now, just do it ourselves: