Hacker News new | ask | show | jobs
by atemerev 637 days ago
These sorts of optimizations can and should be handled by a (sufficiently smart (tm)) compiler.

Common Lisp/SBCL is usually sufficiently smart. I know not everyone likes Common Lisp, but at least I would have tested it with something more performant that Guile, like Chicken Scheme (my favorite!), Chez Scheme, etc.

I like Guile and its purpose as a universal scripting language. However, its performance issues are well known. Even compared to other scripting-first languages (Lua, Perl, Python etc).

6 comments

I think that is why this blog is particularly interesting to me. As one of the other comments to this posting said, it is nice to see an analysis/detailed description of working to optimize code where the first step is not to rewrite in a language with a presumed better performance baseline. Also, I think there is also some props to be given for continuing to work within Guile's somewhat spartan tooling to do the optimization work, instead of switching to a language that may have more/better tooling for the task.

Not to take away from the general comparisons between various Lisp flavors and between various scripting languages (an activity I engage in quite often), but your lead off line is more prescriptive than I find advisable. I don't think a blanket statement that optimizations of runtime behavior of code "should" only be done via a compiler. Some devs enjoy the work, others have varied reasons for doing performance sensitive work in a given language/environment. But at the end of day, doing optimization is a valid usage of developer effort and time if that developer judges it so.

Part of the problem is that raw Scheme is spectacularly underspecified.

It also doesn't help that Schemes like Guile are also interactive. The domain of an interactive language and a "compiled" language are quite different.

Given the entirety of the program made available to the compiler all at once, there are high level derivations that can happen notably through flow analysis to let the compiler make better decisions. But do that in an interactive environment when the rug can be pulled out of any of the assumption the compiler made, and things get messy quite quickly.

One interesting tidbit for Java is that the developers of the compiler advocate "idiomatic" Java. Simply, write Java like Java, and don't try to trick the compiler. Let the compiler developers trick the compiler.

That's evident here in this article when they wrote the function that tests the types of the parameters. Raw Scheme, naturally, doesn't allow you to specify parameter types, not the way you can in Common Lisp, for example. And, either Guile does not have an specific extension to support this, or simply the compiler looks for this type checking pattern at the top of a function to make optimization determinations. On the one hand, it could be more succinct with a specialized facility, but on the other, this is "just Scheme".

So, in effect by checking for the type of the variable, they're implicitly declaring the type of the variable for the "sufficiently smart" compiler to make better decisions.

The counter example is the "define-inline" construct, and thus not standard Scheme (though readily replaced by a "no-op" macro if one was interested in porting the code).

> But do that in an interactive environment when the rug can be pulled out of any of the assumption the compiler made, and things get messy quite quickly.

https://bibliography.selflanguage.org/_static/dynamic-deopti...

> Given the entirety of the program made available to the compiler all at once, there are high level derivations that can happen notably through flow analysis to let the compiler make better decisions. But do that in an interactive environment when the rug can be pulled out of any of the assumption the compiler made, and things get messy quite quickly.

Haskell's GHC does quite well with its 'ghci' interactive environment. GHC is a compiler first and foremost, and as far as I can tell, ghci works by compiling each line you give it one by one? (But even in ghci, you have to abide by the type system of Haskell, so that might help.)

The Common Lisps were always pretty good at combining compiled and interpreted parts, even in the same program. And I think OCaml also does a good job of combining the two approaches?

In my experience, guile these days is often faster than chicken, even compiled (the csi interpreter is dog slow and not suitable for anything but interactive exploration). Chicken is just not a fast implementation.

Plus guile comes with a more comprehensive standard library; on the other hand, chicken's package manager and available packages do make up for that.

Guile has come a long way in the past decade or so! I think your info is quite out of date. Guile's compiler performs a number of state of the art optimizations. It compiles to bytecode so native code compilers like Chez win the race, naturally. The JIT is pretty good, though! Native compilation is on the roadmap for Guile, but maybe somewhat surprisingly we're getting AOT compilation to WebAssembly first. https://spritely.institute/hoot/
It's still much much slower than SBCL which is not surprising given the time & effort that went into the latter. User-guided optimizations (type declarations, stack allocation, intrinsics, machine code generation) in SBCL are also more flexible and better integrated.
Yup, SBCL is quite amazing!
I switched from Guile to SBCL because I really like having things such as (declare (inline my-function)) and (declare (type Double-Float x y z)). Now if only it had case-lambda, named let, and a better deftype which can specify members of classes and/or structs.
The language-hack of the "the" operator in Common Lisp, whereby `(the fixnum (+ 5 7))` signals that the result of (+ 5 7) should be an integer is so ... lispy.
Isn't Racket the 'default' Scheme? (Even though it's no longer called Scheme.)
Extremely easy interop with native code is the main selling point of guile IMO. You just link in guile as a library and can have C code call scheme code and vice versa. Makes it great for any native program that needs an embedded scripting language (much like Lua). Does Racket support that use-case?
I haven't done FFI with Racket, but https://docs.racket-lang.org/foreign/index.html looks reasonably approachable?
That seems to let you call C functions from Racket, but I don't see how it lets you embed Racket in a C program and call Racket functions from C. So it's at best half a solution, unless I'm missing something.
I don't think there's a real 'default' Scheme, like Chez is probably the implementation which generates the fastest code, but if I'm not mistaken it only implements the R6RS spec, Guile is quite performant and supports both R6RS and R7RS Small, Chicken has a bunch of libraries (the 'eggs'), but I think it's R5RS (I may be wrong), and of course GNU/MIT Scheme is what you want to follow along with MIT publications working in Scheme (like Structure and Interpretation of Computer Programs, Structure and Interpretation of Classical Mechanics, and The Art of the Propagator). There's also Gauche, which I believe is the most conformant implementation to the various Colour Dockets for R7RS Large (Gerbil may also be fully conformant).
You can download a package to use R7RS Small in Racket. https://github.com/lexi-lambda/racket-r7rs
For SICP, the best option is probably Racket with the sicp language package.

I can't recommend MIT Scheme for anything these days; it's just missing too many things that I feel are required for real work in Scheme, and has too many idiosyncrasies and quirks. Even using it to run a standalone program written in Scheme is a pain.

Racket is now built around Chez.
Which was a great idea, bootstraped languages always takes the usual "but you depend on XYZ" that haters always bring into the discussion.

Additionally they allow to prove a point.

For example, is writing compilers systems programming or not?

Thanks for the context! It's been a while since I did serious work in the Lisps. (I've moved on to the ML family.)

> [...] GNU/MIT Scheme is what you want to follow along with MIT publications working in Scheme (like Structure and Interpretation of Computer Programs, Structure and Interpretation of Classical Mechanics, and The Art of the Propagator).

Definitely, though I suspect if you need a language that's exactly what's written in the text, you are probably missing the point? At least for SICP, I haven't looked into the others as closely. (Part of) the point being learning wider concepts.

I almost feel like you get more out of the book, if you do the exercises in a mix of JavaScript and Python. Not because those are better languages, just the opposite: because it forces you to understand the concepts well enough to translate them.

In SICM they frequently make use of a kind of implicit applicative lifting (I can't remember what they call it) where you apply a vector-of-functions as if it were a function itself. In psuedo-Haskell:

    lift :: Vec (a->b) -> (a->Vec b)
    lift [] a = []
    lift f:fs = (f a):(lift fs $ a)
so that you can write natural-looking multidimensional physics expressions like

    ((fx fy fz) r)
without having to invoke macros or restructure the expression to please the compiler. I dearly wish you could do this in another scheme but so far I haven't found one. Iirc it's required for using the magnificent `scmutils` package too.

For example, `guile-scmutils`[0] says:

> Functionality not available in the port:

> Scheme extension to allow applying vectors/structures as procedures. For example rather than

    1 ]=> (pe ((up (literal-function 'x) (literal-function 'y)) 't))
    (up (x t) (y t))
> you must use

    guile> (pe ((lambda (t) (up ((literal-function 'x) t) ((literal-function 'y) t))) 't))
    (up (x t) (y t))
[0] https://www.cs.rochester.edu/~gildea/guile-scmutils/
Clojure has a really nice `juxt` function for that, which I have an implementation of in my `.guile` file.

  (define (juxt . fns)
    (lambda args
      (map (lambda (fn) (apply fn args)) fns)))
lift = sequenceA @[] @(a ->)
Did you do that by hand? I guessed you used the classic `pointfree` program, bit that gave

    lift = fix ((`ap` tail) . (. head) . flip ((.) . liftM2 (:)))
As far as I know it's not possible to get this functionality in Haskell even with clever instance magic, but I'd love to be proved wrong.

    lift = flip $ \a -> ($a)
At this point it's a separate language from Scheme. I think Chez Scheme tends to be the "default" recommendation.
No