The Cognitive Burden of Garbage Collection vs Move Semantics

Many people feel that Rust’s borrow checker introduces too much cognitive overhead, and that it must therefore reduce productivity. This is something I strongly disagree with. In fact, I would argue that it reduces cognitive overhead by unifying memory and resource management.

In Garbage Collected languages there is far more manual, error-prone work that the developer is responsible for because, somewhat (IMO very) unintuitively, the only garbage that a GC handles is memory.

This post is going to demonstrate this using Java, but other languages with a GC like Python and Go have this same problem, just replace try-with-resources with defer for Go and with for Python.

Memory is not the Only Resource

Any non trivial program is almost certainly going to do more than handle just memory - file handles, network connections, database clients, threads, etc are all a part of any program’s bag of resources to manage. Errors related to managing these resources are problematic - I know I’ve certainly run into my fair share of resource leaks leading to a process crashing in the middle of the night.

Java, like many other languages, uses a garbage collector (GC) to automatically manage memory. While this eliminates the pitfalls of manual memory management, it also means that we have two methods of managing resources - one for memory, one for the rest. And it’s not always clear (certainly it’s very unclear without an IDE, such as when performing a code review in Github) which method is appropriate to use.

For non-memory resources, Java provides constructs like **try-with-resources** and **AutoCloseable**, but the developer has to know when and where to use them. And that’s the crux of the issue: it’s not always clear who is responsible for cleaning up these resources, leading to potential confusion, errors, and certainly leading to cognitive overhead.

Complex Resource Management in Java

Consider a simple scenario in Java where you establish an HTTP connection, provide that connection to a gRPC channel, and then provide that channel to a gRPC Client:

HttpClient httpClient = HttpClient.newHttpClient();
try(
    GrpcChannel grpcChannel = GrpcChannel.builder(httpClient).build();
    GrpcClient grpcClient = GrpcClient.builder(grpcChannel).build();
) {
        // Use the gRPC client here
}

As humans we can see that this HttpClient is only used in one place - the GrpcChannel. We’ve provided it as an argument and never need to use it again. If the HttpClient had been a memory-only construct we could assume this all works just fine - after all, the Garbage Collector would have no problem understanding that it is never used again.

But HttpClient isn’t memory. Despite GrpcChannel and GrpcClient owning that resource they have no way of releasing it. We have a leak here.

Now, your IDE may help here by pointing out that HttpClient implements AutoClosable. Certainly Intellij seems to do well here - but that doesn’t change the fact that the developer is forced to manage this situation.

You could remedy this situation by moving the HttpClient into the try-with-resources, but this mismatch of resource handling is complex and involves a lot of boilerplate to deal with. And this is just a simple, contrived example - consider cases where a class holds onto an AutoClosable field, complicating ownership further.

The inexpressible ownership is burdensome.

Contrast to Rust

In contrast, Rust introduces a unified approach to managing resources through its ownership model and RAII principles. When an object goes out of scope, Rust automatically cleans up the resources associated with it, both memory and non-memory. Here’s how the same scenario could look in Rust:

let http_client = HttpClient::new();
let grpc_channel = GrpcChannel::new(http_client);
let grpc_client = GrpcClient::new(grpc_channel);

// Use the gRPC client here

// Once out of scope, Rust automatically cleans up grpc_client, and all the resources leading up to it - whether they're memory or not

Rust’s approach provides clear ownership of resources, making it apparent who is responsible for releasing them. Furthermore, resources are automatically released when they go out of scope, ensuring no resource leaks. This eliminates the aforementioned complexity associated with managing non-memory resources in languages with a GC.

We don’t even have to define the scope of our variables - it can change based on the usage. If we returned a variable we’d be saying “ok caller, I give you ownership”, if we didn’t move it anywhere we’d retain it, and, as we see here, if we do move it, we move the responsibility with it.

This unified approach introduces far less cognitive overhead. There is one way to do things, it’s handled for you, and you have one, flexible system for changing ownership.

Conclusion

My personal opinion, and my experience as well, has been that many tools like GCs that try to help you actually introduce far more complexity - but I constantly encounter statements like “Rust just won’t be as productive as a GC’d language” that flat out aren’t the case for me or other Rust devs I know.

GC is just one example. Languages that try to hide pointer semantics for you also introduce a massive amount of complexity, often hiding copies from you in a way that makes you ask the question “if I mutate this thing is it just mutating my copy or someone else’s?” - the subject of another post, I think.

Now I can assume that I’m going to get some responses like: “OK but it’s not that hard, you just learn to do it, or use tools to catch these problems”.

And that’s fine - I find it to be this annoying extra task when writing code where I have to think “wait is this thing supposed to be closed? k, I’ll add it to my ever-growing try-with-resources” but it’s not like I can’t write Java.

The point isn’t “Java is unusable”, it’s that the solutions that GC brings have their own cognitive burden and that, in my experience, when something tries to do things for you automatically, unless it can do everything for you automatically, it’s going to end up making things worse.

Anyway that’s it.



blog comments powered by Disqus

Published

09 June 2023

Categories