The speed tests appear to include the startup time of the virtual machine. We all know Java is slow to start up and Go is fast in this respect. That's not useful information.
Rather than bias the results this way, much more interesting would be to test what the speeds are (1) after the VMs have launched, and also (2) when the VMs are hot, that is, Java has compiled your Lisp classes.
startup time is key to being able to write one-off programs/tasks and use them in workflows.
It doesn’t matter as much for long lived programs, but I don’t think it’s fair to frame this as “bias the results”
Yup, once hotspot kicks in on the JVM, a toy language is going to have a hard time competing with that performance. It’s definitely disingenuous to include JVM start up time in any benchmark. I used to run clojure benchmarks in a REPL and I would watch as the functions I was testing dramatically increased in performance over a few invocations. JIT compilation is pretty incredible.
Given sufficient time and a sufficiently expert programmer. It is not that easy to beat Java for complex programs where your workload is really variable and can’t use the usual tools like arenas.
There are some workloads where Java's JIT can beat C, C++, Fortran AOT compilation. They are probably not very common, but stuff like un-inlining can really save on instruction cache misses in workloads where the most likely path is consistent for fixed periods of time, but changes relatively often (for something like `if COND { foo(); } else { bar(); }` in a hot loop, where COND is sometimes true for a while than false for another while).
Of course, Java also has a lot of overhead from the language and implementation level, because of things like no generic lists of integers, no struct embedding leading to lots of pointer-chasing, a tendency for over-allocation, and direct overhead of the advanced JIT itself - performance counters, dummy instructions to support de/re-optimizations on the fly etc.
I can just as well claim that ignoring startup with a small hot loop would unfairly favor a JIT compiler, since it ignores problems that would crop up in real world scenarios such as poor code locality.
Basically what you're suggesting would be the absolute best-case for JIT.
He does mention that he is looking for language to use for cli with fast startup. For cli and serverless cold start times become important so much so that clojure (non Graal complied) isn't usable for those scenarios.
Ya, the AppCD for OpenJDK is what I was trying last time, I think on JDK 11, and frankly I couldn't figure it out, like you have to run with a command, dump something to some path, then re-run with specifying the same path and I don't know it didn't seem it was working last time and it was pretty cumbersome.
Eh, the JVM startup time is much less these days (read: these last several years), however loading Clojure is quite slow comparatively and that's what ends up dominating. On my machine, here's a hastily written Fact.java:
import java.math.BigInteger;
public class Fact {
public static BigInteger fact(BigInteger n) {
if (n.equals(BigInteger.valueOf(1))) {
return BigInteger.valueOf(1);
} else {
return n.multiply(fact(n.subtract(BigInteger.valueOf(1))));
}
}
public static void main(String args[]) {
System.out.println(fact(BigInteger.valueOf(100)));
}
}
With OpenJDK 11.0.14, `time java Fact.java` gives real/user/sys of 0.267s/0.654s/0.033s. But that's not compiling the file first which some programmers aren't even aware is a thing Java can do now, if I do the usual thing of compiling with javac then on the class file `time java Fact` I instead get 0.050s/0.043s/0.010s. For Clojure, `time java -jar /usr/share/clojure-1.10/lib/clojure.jar fact.clj` gives 0.468s/1.069s/0.056s.
Edit: for fun since the author mentioned Common Lisp, `time sbcl --no-sysinit --no-userinit --script fact.lisp` gives 0.005s/0.003s/0.002s. If I make a binary out of this instead, `time ./fact` gives 0.003s/0.002s/0.001s`. Since even a time of an empty C program's a.out can sometimes give numbers like 0.001s/0.000s/0.001s I'm clearly running into precision limits of the time command. (CL itself has a time macro that will helpfully give you a processor cycles count, about 290,000 on my machine for this.) I think the author is overestimating the difficulty of adding features to Common Lisp relative to what they plan to do with this toy; CL for scripting works great too.
> You may have noticed the lack of character strings in the feature table, above. Many interesting Lisp programs don't use traditional strings (arrays of characters encoded as bytes)
Thank you for that. I'm taking a look athe the parser, lexer and generator.
Question about the current usage of it, can you take some app LISP code made in some other LISP environment and make it work in this one?
Not sure about Gisp specifically, but in my experience authors of transpilers tend to ‘reinvent the wheel’, in the form of low-level constructs—which usually reflect those of the target language. Especially since transpilers explicitly rely on the target language's standard library.
I'm guessing that various incarnations of the Scheme language may be most compatible and interchangeable.
Without strings, one will probably want to manipulate atoms in various ways. As a start, l1 introduces the notion of "splitting" (creating a list from an atom or number) and "fusing" (joining such a list back together again)
While prizing any FLOSS project I'm curious why... I can understand Clojure, giving Lisp on top of the enormous Java codebase have some practical reasons, but Go so far have a relatively small public set of libs so why instead of a Lisp written in Lisp, like SBCL?
Huh? Go has an enormous and high quality std lib and even larger set of community libs. On top of that its popularity is exploading. I’ve never seen a language rise in popularity as much as Go.
Have a high quality stdlib, but nothing other languages do not have. There are many community libs, but again nothing that special, also the popularity rise for a language transpiled on top is not much a thing...
Java exists today in many places where change codebase is a nightmare and keep up software is no less a nightmare so having a more manageable language able to insert itself in the old ecosystem is valuable, but on a moderately new language with nothing so big to be in a state of needing a nightmarish replacement why?
It's like Hy for Python: it's nice, a programming exercise but for what more than the technical piece? How many Python programs need a new language while remain on their own python VM? How many Python programmers want lisp with Python underneath?
I think the real issue is that Go is its runtime, including garbage collection but also coroutine scheduling, syscall management (syscalls effectively enter 'gospace' before kernelspace), etc. Go's docs liken it to crt0, which is entirely dishonest. You can't have verifiable deterministic programs with a runtime like this which extends way beyond a small setup routine. That's generally 'fine in practice' for application development - especially the Python-level programs which are generally written in Go - but for building another language (interpreter) atop it, it strikes me as madness.
For some reason, the benchmark done in this post has so much bad faith, that it turned me off completely from the actual project. It seems just too naive or disingenuous and it kind of loses my trust.
Given their similar concurrency stories, I've felt that the Go VM (is it GVM?) made a better match for Clojure. Of course, Clojure was released before Go so not a real possibility at the time.
I thought so too, and worked on this for a while, but Clojure compiles at runtime, while Go does source generation then compiles at build time. I found what was essentially a Clojure-to-Go source transpiler to be surprisingly limited.
Go doesn't have a VM, it is always pre-compiled to native binaries specific to the target architecture/platform you want to run it on.
It also generally speaking doesn't have an intermediate representation that you could target directly, because it does a lot of optimization directly on the GO AST. Though you might be able to have a different language compile to its SSA directly, but it would not be easy at all.
The GO runtime also doesn't allow for any dynamic code loading, so you wouldn't be able to have a REPL.
Yes, strictly speaking you are correct. I guess I consider any language that has built-in GC as effectively a VM (i.e. there's a lot of code being lugged around you don't control).
54 comments
[ 1.2 ms ] story [ 121 ms ] threadRather than bias the results this way, much more interesting would be to test what the speeds are (1) after the VMs have launched, and also (2) when the VMs are hot, that is, Java has compiled your Lisp classes.
It's not since startup time matters for CLI applications.
Not if you compare to AOT for languages who lack garbage collectors. C, C++, Fortran would still be quite faster.
Of course, Java also has a lot of overhead from the language and implementation level, because of things like no generic lists of integers, no struct embedding leading to lots of pointer-chasing, a tendency for over-allocation, and direct overhead of the advanced JIT itself - performance counters, dummy instructions to support de/re-optimizations on the fly etc.
Basically what you're suggesting would be the absolute best-case for JIT.
Can you link to something or explain how to use JIT caches?
https://docs.oracle.com/en/java/javase/18/vm/class-data-shar...
Or how to use it alongside Spring,
https://medium.com/@toparvion/appcds-for-spring-boot-applica...
For OpenJ9,
https://developer.ibm.com/articles/optimize-jvm-startup-with...
On Android, ART does it automatically since Android 7,
https://source.android.com/devices/tech/dalvik/jit-compiler
Edit: for fun since the author mentioned Common Lisp, `time sbcl --no-sysinit --no-userinit --script fact.lisp` gives 0.005s/0.003s/0.002s. If I make a binary out of this instead, `time ./fact` gives 0.003s/0.002s/0.001s`. Since even a time of an empty C program's a.out can sometimes give numbers like 0.001s/0.000s/0.001s I'm clearly running into precision limits of the time command. (CL itself has a time macro that will helpfully give you a processor cycles count, about 290,000 on my machine for this.) I think the author is overestimating the difficulty of adding features to Common Lisp relative to what they plan to do with this toy; CL for scripting works great too.
For Java 11, the docs are on https://docs.oracle.com/en/java/javase/11/vm/class-data-shar...
Latest versions have other improvements like PGO info as well.
More specifically, Clojure —
https://stuartsierra.com/2019/12/21/clojure-start-time-in-20...
Is this true?
I'm guessing that various incarnations of the Scheme language may be most compatible and interchangeable.
"Fission" better than "splitting" surely? ;)
Java exists today in many places where change codebase is a nightmare and keep up software is no less a nightmare so having a more manageable language able to insert itself in the old ecosystem is valuable, but on a moderately new language with nothing so big to be in a state of needing a nightmarish replacement why?
It's like Hy for Python: it's nice, a programming exercise but for what more than the technical piece? How many Python programs need a new language while remain on their own python VM? How many Python programmers want lisp with Python underneath?
I can identify having this impulse after doing things with Rust but fantasizing about a LISP cousin: Smalltalk.
Now that you mentioned Go it is a more natural thing to try due to its garbage collection already done there. Thanks por pointing that out.
What restrains me a bit of starting any effort in this direction is Pharo's current performance:
Said that, I do not want to sunk time into trying to improve something, only to find out tha it will perform worst.
Maybe the compromise is to try to do a minimal proof of concept only manually transpilig a couple of objects from Smaltalk to Go and measure that?
In any case, shouldn't your measurement be made the same way John Jacobsen seems to have made measurements —
By the way, I'm not recognizing that command, I'm curious, what smalltalk is that you're using there?
I don't think we know about the hardware used for the other measurements — so sketchy comparison.
> … excluding system startup/shutdown…
Why would you do that when the other measurements included startup/shutdown?
https://github.com/eigenhombre/l1/
> … not recognizing that command…
Cincom Smalltalk, VisualWorks® 8.3 run with `time` from a terminal window command line, like the other measurements.
It also generally speaking doesn't have an intermediate representation that you could target directly, because it does a lot of optimization directly on the GO AST. Though you might be able to have a different language compile to its SSA directly, but it would not be easy at all.
The GO runtime also doesn't allow for any dynamic code loading, so you wouldn't be able to have a REPL.
Please try with the latest 0.8.2:
$ time bb /tmp/fact.clj
bb /tmp/fact.clj 0.02s user 0.01s system 90% cpu 0.035 total
Wrapping the last expression with `(time ...)` yields: 0.285566 ms