You can let it 6x faster by add more code, comment some old code, without touching the hot loop.
I also added a Julia version (same as C code) for compare, with 500k as parameter.
First version on my computer:
=== C (GCC) ===
-0.169237033
timing: 24 ms
=== Common Lisp (SBCL) ===
-0.169237033
timing: 32 ms
=== Racket ===
-0.169237033
timing: 875 ms
=== Julia ===
-0.169237033
timing: 29 ms
First optimization is, define body as flvector instead of struct, then comment the struct definition:
; (struct body (x y z vx vy vz mass) #:prefab #:mutable)
(define (body x y z vx vy vz mass)
(flvector (real->double-flonum x)
(real->double-flonum y)
(real->double-flonum z)
(real->double-flonum vx)
(real->double-flonum vy)
(real->double-flonum vz)
(real->double-flonum mass)))
(define (body-x b) (unsafe-flvector-ref b 0))
(define (body-y b) (unsafe-flvector-ref b 1))
(define (body-z b) (unsafe-flvector-ref b 2))
(define (body-vx b) (unsafe-flvector-ref b 3))
(define (body-vy b) (unsafe-flvector-ref b 4))
(define (body-vz b) (unsafe-flvector-ref b 5))
(define (body-mass b) (unsafe-flvector-ref b 6))
(define (set-body-x! b v) (unsafe-flvector-set! b 0 v))
(define (set-body-y! b v) (unsafe-flvector-set! b 1 v))
(define (set-body-z! b v) (unsafe-flvector-set! b 2 v))
(define (set-body-vx! b v) (unsafe-flvector-set! b 3 v))
(define (set-body-vy! b v) (unsafe-flvector-set! b 4 v))
(define (set-body-vz! b v) (unsafe-flvector-set! b 5 v))
(define (set-body-mass! b v) (unsafe-flvector-set! b 6 v))
The result is 666ms, 1.3x faster.
Second optimization is, use flonum operations to replace old arithmetic operations in hot loop. Require racket/flonum, then add these code to advance function, before any other code:
(define + fl+)
(define - fl-)
(define * fl*)
(define / fl/)
(define sqrt flsqrt)
The result is 231ms, 3.78x faster than the first version.
In the first for loop of advance, you used set! to modify dx, dy, dz, dsq, and mag. But you can actually define them in the loop, and get rid of set!. The third optimization is, add an evil macro to advance function:
(define-syntax-rule (set! x val) (define x val))
It replaces set! with define (never do this in real code). The result is 136ms, 6.4x faster than the first version! It still cannot compare to C, Common Lisp or Julia, but not that slow.
My extra experiment is extract many duplicate (vector-ref bod-vec i) to a variable, then use this variable:
(for* ([i (in-range NBODIES)]
[j (in-range (add1 i) NBODIES)])
(define bi (vector-ref bod-vec i))
(define bj (vector-ref bod-vec j))
(define dx (- (body-x bi) (body-x bj)))
(define dy (- (body-y bi) (body-y bj)))
(define dz (- (body-z bi) (body-z bj)))
(define dsq (+ (* dx dx) (* dy dy) (* dz dz)))
(define mag (/ dt (* dsq (sqrt dsq))))
(set-body-vx! bi (- (body-vx bi) (* dx mag (body-mass bj))))
(set-body-vy! bi (- (body-vy bi) (* dy mag (body-mass bj))))
(set-body-vz! bi (- (body-vz bi) (* dz mag (body-mass bj))))
(set-body-vx! bj (+ (body-vx bj) (* dx mag (body-mass bi))))
(set-body-vy! bj (+ (body-vy bj) (* dy mag (body-mass bi))))
(set-body-vz! bj (+ (body-vz bj) (* dz mag (body-mass bi))))))
This result is 49ms. Only 2x slower than C.