1

Consider this code snippet which calculates the delta time since the last frame:

(defn game-loop [window game] (let [last-frame (atom (GLFW/glfwGetTime))] (while (not (GLFW/glfwWindowShouldClose window)) (let [now (GLFW/glfwGetTime) delta (- now @last-frame)] (reset! last-frame now) 

My understanding of an atom is that when I replace it's value, the old value will be left for garbage collection (unless referenced elsewhere). That would mean that I'm producing garbage at 60 frames a second.

If this is correct, is there a way to avoid that? Is there a way to write to a variable without writing Java code? Or is there some neat trick to calculate delta time without using an atom?

1
  • 1
    I wouldn't worry about it - the garbage collector does amazing things. You may enjoy learning about volatile! (clojuredocs.org/clojure.core/volatile!), but it won't make any difference in this particular case. Commented Sep 9 at 21:50

2 Answers 2

1

If you replace some object with a different object and the first object isn't referenced by anything, it will be a subject to GC. That happens regardless of what "container" you use to store that object - an atom, a volatile, some collection, a plain Java field, etc. It's absolutely irrelevant.

Don't worry about it, that's par for the course with JVM.

However, two things:

  • I would use a volatile! there instead of an atom. Atoms are much more useful for multithreading
  • If you do end up using an atom, don't deref+reset it, use swap!. Just make sure that the function that you pass into it is free of side-effects. So in this case it would be something like:
(defn game-loop [window game] (let [last-frame (atom (GLFW/glfwGetTime))] (while (not (GLFW/glfwWindowShouldClose window)) (let [now (GLFW/glfwGetTime)] (swap! last-frame (fn [last-frame-val] (let [delta (- now last-frame-val)] ... ;; Returning the new value of `last-frame`. now))))))) 

But again, only if that ... is free of side effects. And as much computation that doesn't depend on last-frame should be done outside of that fn to reduce the amount of time spent in swap!.

Alternatively, you can avoid mutation altogether by using loop and recuring in there only if the condition is met. In fact, this is most definitely what I'd do, even instead of using a volatile!.

Sign up to request clarification or add additional context in comments.

4 Comments

Why would you not reset and instead use swap? It was my understanding that swap is useful when I want to compute the new value based on the old, which in this case I don't.
When you need atoms, you pretty much never want to deref them and then reset them - that just creates race conditions. swap! prevents that. The fact that you don't use the old value to compute the new one is inconsequential. Of course, there's even more nuance here, but it's not related to the original question.
I like the idea of using loop/recur instead of the while loop. However, you said only if the condition is met. I assume that means no side effects. The game loop does update the screen at some point; there will even be network traffic in the future. Thus, there are side effects. Should this prevent me from using loop/recur? If so, why?
By "condition" I meant your current check in while - the (not (GLFW/glfwWindowShouldClose window)) that I was simply too lazy to copy that time. You can have all kinds of side-effects in loop, it's totally fine.
1

With modern computers and JVMs, garbage is not a problem until you are way over 1 million objects per second.

Please remember, "Premature optimization is the root of all evil!" (at least in software terms).

Here is a short timing in Clojure (using the Tupelo Clojure library):

(ns tst.demo.core (:use demo.core tupelo.core tupelo.test) (:require [tupelo.profile :as prof])) (verify (prof/with-timer-print :loop (let [count (atom 0)] (dotimes [i 100000] (swap! count inc)) (spyx @count)))) 

with results:

----------------------------------- Clojure 1.12.0 Java 24.0.1 ----------------------------------- Testing tst.demo.core (clojure.core/deref count) => 100000 :with-timer-print :loop 0.002367 

So you see we can increment an integer in a Clojure atom over 100k times in less than 3 milliseconds (or about 400*100k => 30 million times/sec).

The timing appears to be linear. If we up the count to 1e6 (one million), it takes about 1/40'th of a second:

Testing tst.demo.core (clojure.core/deref count) => 1000000 :with-timer-print :loop 0.025091 

While this loop could be sped up in many ways, the expenditure of labor hours at this stage is premature and wasteful.

2 Comments

When does garbage collection run? Does it run very closely to the production of garbage? Or is it possible that the garbage collection isn't even triggered before the timing is done?
It is possible, yes. There are different GC available on JVM that you can switch between, and how they work depends also on the JVM settings. It's even possible to completely turn off GC. But really, that's not something you should concern yourself with until you stumble across a problem where GC makes things worse for you. Chances are, you'll never even encounter such a problem.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.