Skip to content

Conversation

Jason94
Copy link
Contributor

@Jason94 Jason94 commented Oct 8, 2025

This PR adds a base Env :env monad to environment.lisp. Previously Env :env was just an alias for EnvT :env Identity. Haskell defines Reader like this, and they can get away with it. GHC does a lot of work behind the scenes to fuse lambda calls, so wrapping the Identity monad is zero-cost at runtime for them. Coalton doesn't have that, so it's not.

Here's a little microbenchmark that takes an Integer as the environment and a list of ints as an argument, and maps adding the environment Integer to all of the ints in the list inside the monad.( It does this very inefficiently, so it binds for every element in the list.)

(cl:in-package :cl-user)
(defpackage :bench-env
  (:use
   #:coalton
   #:coalton-prelude
   #:coalton-library/monad/environment
   #:coalton-library/monad/identity
   )
  )

(in-package :bench-env)

(named-readtables:in-readtable coalton:coalton)

(coalton-toplevel
  (define context 100)

  (declare add-env (MonadEnvironment Integer :m => List Integer -> :m (List Integer)))
  (define (add-env ints)
    (rec % ((rem ints)
            (accum Nil))
      (match rem
        ((Nil) (pure (reverse accum)))
        ((Cons x rem)
         (do
          (n <- ask)
          (% rem (Cons (+ n x) accum)))))))


  (declare test-env (List Integer -> Unit))
  (define (test-env ints)
    (run-env (add-env ints) context)
    Unit)

  (declare test-envT (List Integer -> Unit))
  (define (test-envT ints)
    (run-identity (run-envT (add-env ints) context))
    Unit)

  (declare test-both (Unit -> Unit))
  (define (test-both)
    (let ints = (range 0 10000000))
    (lisp :a (ints)
      (cl:time (coalton (test-env (lisp :a () ints)))))
    (lisp :a (ints)
      (cl:time (coalton (test-envT (lisp :a () ints)))))
    Unit)
  )

Here are typical results on my machine, compiled in release mode:

Evaluation took:
  1.186 seconds of real time
  1.182656 seconds of total run time (1.007186 user, 0.175470 system)
  [ Real times consist of 0.547 seconds GC time, and 0.639 seconds non-GC time. ]
  [ Run times consist of 0.543 seconds GC time, and 0.640 seconds non-GC time. ]
  99.75% CPU
  4,382,138,882 processor cycles
  1,919,869,824 bytes consed
  
Evaluation took:
  1.226 seconds of real time
  1.220518 seconds of total run time (1.082556 user, 0.137962 system)
  [ Real times consist of 0.356 seconds GC time, and 0.870 seconds non-GC time. ]
  [ Run times consist of 0.357 seconds GC time, and 0.864 seconds non-GC time. ]
  99.59% CPU
  4,527,260,615 processor cycles
  2,719,164,848 bytes consed

The base Env runs ~3.26% faster than the EnvT' which actually surprised me that it was so close. I'm not good enough at optimizing CL/Coalton to really know why that might be, other than speculating that the EnvT & Identity monads really don't do that much.
The base Env did cons ~29% fewer bytes than the EnvT version did, which is in line with what I would have expected. Also curious, the base version actually spent more time in GC, but I suspect that's just noise or a bad test setup on my part.

@Jason94
Copy link
Contributor Author

Jason94 commented Oct 9, 2025

I re-ran the benchmark with 12M iterations, in the SBCL console. Previously I was testing in Sly inside Emacs, which I noticed was leading to some funky freezes. The results were much closer to what I expected. I also tested with the heuristic inline on/off, and it made a very small but possibly statistically significant speed-up turning it on. (I'm not surprised the automatic inliner didn't have much impact, because the monad code is aggressively (inline) annotated.)

Test – 12,000,000 iterations

Expand benchmark table
test evaluation_index real_time_seconds gc_time_seconds non_gc_time_seconds processor_cycles bytes_consed avg_real_time (s)
test-env 1 1.449 0.7 0.749 5350510319 2303851120
test-env 2 1.022 0.347 0.675 3773787805 2303875024 1.00344444444444
test-env 3 1.073 0.418 0.655 3962464643 2303851152
test-env 4 0.997 0.357 0.64 3679401138 2303842304
test-env 5 0.963 0.311 0.652 3557912970 2303883904
test-env 6 1.05 0.403 0.647 3876228632 2303851248
test-env 7 0.966 0.326 0.64 3566461968 2303875072
test-env 8 0.977 0.336 0.641 3609443166 2303851216
test-env 9 0.982 0.34 0.642 3626700521 2303907792
test-env 10 1.001 0.345 0.656 3693185784 2303818496
test-envT 1 1.39 0.477 0.913 5133118632 3263214272
test-envT 2 1.306 0.377 0.929 4824995212 3263189584 1.30822222222222
test-envT 3 1.323 0.399 0.924 4883256152 3263199104
test-envT 4 1.278 0.363 0.915 4720169883 3262978960
test-envT 5 1.371 0.46 0.911 5063699824 3262997184
test-envT 6 1.307 0.383 0.924 4825740540 3262997280
test-envT 7 1.318 0.388 0.93 4867550873 3263224528
test-envT 8 1.271 0.356 0.915 4692395315 3262997216
test-envT 9 1.305 0.397 0.908 4817381833 3262978928
test-envT 10 1.295 0.377 0.918 4784951222 3263030016
test-env (inliner disabled) 1 1.473 0.734 0.739 5442874825 2303851280
test-env (inliner disabled) 2 1.04 0.375 0.665 3841716845 2303851216 1.02255555555556
test-env (inliner disabled) 3 0.959 0.324 0.635 3542639888 2303875008
test-env (inliner disabled) 4 1.078 0.44 0.638 3979465403 2303851216
test-env (inliner disabled) 5 0.991 0.345 0.646 3660430017 2303875040
test-env (inliner disabled) 6 0.999 0.357 0.642 3689732241 2303883920
test-env (inliner disabled) 7 0.998 0.36 0.638 3687453041 2303875056
test-env (inliner disabled) 8 1.085 0.369 0.716 4005687118 2303851152
test-env (inliner disabled) 9 1.083 0.427 0.656 3998785915 2303842272
test-env (inliner disabled) 10 0.97 0.326 0.644 3584094207 2303879520
test-envT (inliner disabled) 1 1.348 0.411 0.937 4980579509 3263199056
test-envT (inliner disabled) 2 1.344 0.416 0.928 4961919891 3262978960 1.35755555555556
test-envT (inliner disabled) 3 1.283 0.359 0.924 4736679357 3262997184
test-envT (inliner disabled) 4 1.34 0.41 0.93 4949700789 3262997280
test-envT (inliner disabled) 5 1.341 0.408 0.933 4950213831 3263224528
test-envT (inliner disabled) 6 1.285 0.367 0.918 4746497825 3262997216
test-envT (inliner disabled) 7 1.321 0.398 0.923 4881190331 3262978928
test-envT (inliner disabled) 8 1.47 0.42 1.05 5428045965 3263030016
test-envT (inliner disabled) 9 1.531 0.538 0.993 5653814711 3263151888
test-envT (inliner disabled) 10 1.303 0.391 0.912 4809611574 3262978960
The first row looked like it was an outlier (possibly because of some processor or SBCL warm-up time, but I honestly don't know how that works at that level.) Removing the first row of each test, the `Env` code was ~23.3% faster than the `EnvT :e Identity` code, which is in line with what I expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant