Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> There's nothing inherently slow about LISP.

> In other words, whatever assumptions I had about higher level languages and/or garbage collection being bad for game dev were mostly proven wrong.

I'm afraid I have to disagree on that point. GOAL is not LISP -- it just looks a bit like it, and draws a lot of ideas from it. However, LISP is ultimately dynamically typed, while GOAL is statically typed. This means the compiler can generate fast code ahead of time, LISP can't.

GOAL also does not use runtime garbage collection in the same way a LISP system does.



Yeah, the runtime for GOAL was much closer to C/C++ than LISP. The pre-processor was pretty much a full Scheme implementation, and had all kinds of dynamic cons cell allocations and garbage collection, but not the generated code itself.

(In fact, C and GOAL code almost used the same ABI. IIRC, GOAL always had the GP(?) register pointing at the symbol table for fast access, but gcc MIPS code expected it to be something else. If you wanted to be able to call back and forth, you had to write tiny shim functions).


> However, LISP is ultimately dynamically typed, while GOAL is statically typed. This means the compiler can generate fast code ahead of time, LISP can't.

LISP (of 1958) can't, but Lisp can. Optimizing compilers for Lisp were invented decades ago.

Surprise: Common Lisp has types and type declarations since day one (1984). The type system is not what you would expect from, say, Haskell. But it allows the developer to define and specify types.

Additionally one can set optimization policies (debug, space, safety, ...), declare functions to be compiled inline or request to have data stack allocated. And so on...

Compilers which take advantage of type declarations and type inference to create optimized code exist also since that time.

See the Common Lisp Hyperspec chapter on types:

http://www.lispworks.com/documentation/HyperSpec/Body/04_.ht...

> GOAL also does not use runtime garbage collection in the same way a LISP system does.

There are several Lisp systems which do similar things (manual memory management) like GOAL. Mostly they were/are used to develop certain types of applications - like GOAL.

Let's see how sbcl, ( http://sbcl.org ) handles types at compile time: it clearly identifies and describes where it has not enough information for best optimization.

    (declaim (optimize (speed 3) (debug 0)))

    ; a TYPE declaration

    (deftype somebyte ()
      `(integer 0 255))

    ; an untyped function

    (defun add1 (a b)
      (+ a b))

    ; declararing the types of function ADD2
    ; but one argument remains of general type. 
    ; arguments are of type SOMEBYTE and T.
    ; T is the most general type.
    ; Return value has type SOMEBYTE.

    (declaim (ftype (function (somebyte t) somebyte)
                    add2))

    (defun add2 (a b)
      (+ a b))


    ; declararing the types of function ADD3

    (declaim (ftype (function (somebyte somebyte) somebyte)
                    add3))

    (defun add3 (a b)
      (+ a b))

You can now see what the LISP (sic!) compiler says, when it can't optimize the code:

    * (compile-file "/tmp/test.lisp")

    ; compiling file "/private/tmp/test.lisp" (written 13 JAN 2017 09:15:03 AM):
    ; compiling (DECLAIM (OPTIMIZE # ...))
    ; compiling (DEFTYPE SOMEBYTE ...)
    ; compiling (DEFUN ADD1 ...)
    ; file: /private/tmp/test.lisp
    ; in: DEFUN ADD1
    ;     (+ A B)
    ;
    ; note: forced to do GENERIC-+ (cost 10)
    ;       unable to do inline float arithmetic (cost 2) because:
    ;       The first argument is a T, not a DOUBLE-FLOAT.
    ;       The second argument is a T, not a DOUBLE-FLOAT.
    ;       The result is a (VALUES NUMBER &OPTIONAL), not a (VALUES DOUBLE-FLOAT
    ;                                                                &REST T).
    ;       unable to do inline float arithmetic (cost 2) because:
    ;       The first argument is a T, not a SINGLE-FLOAT.
    ;       The second argument is a T, not a SINGLE-FLOAT.
    ;       The result is a (VALUES NUMBER &OPTIONAL), not a (VALUES SINGLE-FLOAT
    ;                                                                &REST T).
    ;       etc.

    ; compiling (DECLAIM (FTYPE # ...))
    ; compiling (DEFUN ADD2 ...)
    ; file: /private/tmp/test.lisp
    ; in: DEFUN ADD2
    ;     (+ A B)
    ;
    ; note: forced to do GENERIC-+ (cost 10)
    ;       unable to do inline fixnum arithmetic (cost 2) because:
    ;       The second argument is a T, not a FIXNUM.
    ;       The result is a (VALUES NUMBER &OPTIONAL), not a (VALUES FIXNUM &REST T).
    ;       unable to do inline (signed-byte 64) arithmetic (cost 5) because:
    ;       The second argument is a T, not a (SIGNED-BYTE 64).
    ;       The result is a (VALUES NUMBER &OPTIONAL), not a (VALUES (SIGNED-BYTE 64)
    ;                                                                &REST T).
    ;       etc.

    ; compiling (DECLAIM (FTYPE # ...))
    ; compiling (DEFUN ADD3 ...);
    ; compilation unit finished
    ;   printed 2 notes


    ; /tmp/test.fasl written
    ; compilation finished in 0:00:00.030
    #P"/private/tmp/test.fasl"
    NIL
    NIL


It might also be interesting for readers to see what the resulting code is from these three functions.

I recompiled your code with SAFETY 0 and here is the result. This example also shows off how you can look at the disassembly of any function directly from the REPL.

First, ADD1. Since the compiler doesn't know anything about the types (the arguments could be floating points numbers, rationals, bignums or any other type) so it will simply call the GENERIC-+ function which does all of this. Needless to say, it's much slower than a native addition.

    CL-USER> (disassemble #'add1)
    ; disassembly for ADD1
    ; Size: 14 bytes. Origin: #x2291202A
    ; 2A:       488BD1           MOV RDX, RCX                     ; no-arg-parsing entry point
    ; 2D:       E86EE26EFD       CALL #x200002A0                  ; GENERIC-+
    ; 32:       488BE5           MOV RSP, RBP
    ; 35:       F8               CLC
    ; 36:       5D               POP RBP
    ; 37:       C3               RET
The function ADD2 still has unknown types, so there is not much the compiler can do but to call GENERIC-+ in this case.

    CL-USER> (disassemble #'add2)
    ; disassembly for ADD2
    ; Size: 14 bytes. Origin: #x22911FBA
    ; BA:       488BD1           MOV RDX, RCX                     ; no-arg-parsing entry point
    ; BD:       E8DEE26EFD       CALL #x200002A0                  ; GENERIC-+
    ; C2:       488BE5           MOV RSP, RBP
    ; C5:       F8               CLC
    ; C6:       5D               POP RBP
    ; C7:       C3               RET
Since the compiler knows that both arguments are bytes, it can simply call the ADD instruction. With the exception of some extra bookkeeping needed when returning from the function, this is just as efficient as what a C++ compiler would generate. You can also declare a function as INLINE to allow the compiler to inline the call.

    CL-USER> (disassemble #'add3)
    ; disassembly for ADD3
    ; Size: 12 bytes. Origin: #x22911F4A
    ; 4A:       4801F9           ADD RCX, RDI                     ; no-arg-parsing entry point
    ; 4D:       488BD1           MOV RDX, RCX
    ; 50:       488BE5           MOV RSP, RBP
    ; 53:       F8               CLC
    ; 54:       5D               POP RBP
    ; 55:       C3               RET




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: