Chain of Memory Hogs
As we saw in The Structural Bug, the composition of functions can itself be a problem. On top of that, as we move to containerised apps, where we’re starting to run software on microscopic machines, it becomes more important than it used to be to worry about things like memory consumption and resource usage.
Programming languages like Java and JavaScript allow us to write software in a variety of different ways. It’s easy to get into habits that are bad at runtime without noticing.
Let’s consider the following code:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | void sendData(Data d) { BigObject big = new BigObject(d); BigOtherObject big2 = new BigOtherObject(d); int value = big2.calculateSomething(); calculateAndSend(big, value); } void calculateAndSend(BigObject big, int value) { HugeThing huge = new HugeThing(big, value); String id = huge.generateId(); send(big, id); } void send(...) {...} |
In the above example, let’s use our imaginate a little and assume that there’s some huge overhead for having things of type BigObject
, BigOtherObject
, and HugeObject
. The final send
function gets called by a chain of invocations from the first through to the last. As send
gets called, ALL of the declared large objects are still in memory and cannot be garbage collected.
Chasing the Tail
What we have here is an example of tail-calling. The last thing a function does is call another function. At the time of writing, tail-call optimisation is not available in Java and may or may not be supported by the JavaScript engine you’re currently using.
In a world where these tail calls were automatically turned into a pseudo function return and fresh invocation of the next function, two things would be possible:
- We wouldn’t get deeper into the call stack, occupying more and more stack memory
- We would be able to garbage collect any temporary objects that were no longer needed now we’re doing the next thing
Middle of the Chain
There’s a worse version of the above, where the deeply chained functions somehow are called each in the middle of each other, and the magical get-out-of-jail tail-call optimisation wouldn’t even help us.
What’s The Real Problem?
Sometimes none of this is a problem and it’s just a fact of life.
Often, the real problem is that there’s a lack of clarity of purpose in the code. In general, if we designed our code top down, we might say:
- Initialise data
- Gather next bit of value
- Manipulate that value
- Operate on the result
But in the chain of functions, we’ve accidentally made it look like
- Initialise data
- Do next bit
- Gather next bit of value
- Do next bit
- Manipulate value
- Do…
Even if this was no harm to the runtime, it’s relatively difficult to follow what’s going on as a reader of the code.
Solving The Problem
Now we’ve established the pattern. A chain gang of functions, each adding on a bit and passing it forwards, we can spot it.
The solution is relatively simple. Avoid functions that push the result forwards and try instead to have functions that return something.
This is not the right answer to every programming problem, but it’s a good start.
Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: Chain of Memory Hogs Opinions expressed by Java Code Geeks contributors are their own. |