Ownership, borrowing, lifetimes, parallel programming and unnecessary complexity

(Preamble: This is not really a zig related question, but I know there are some very experienced programmers on this forum and I respect y’all opinions. This post is a collection of some thoughts I had and that came up while writing this.)

Is ownership (and explicit lifetime bounds) just unnecessary complexity?

While watching a video on youtube about rust and its comparison with C++, it got to ownership, borrowing and RAII and thought:

  • I’d avoid automatic deconstruction like the plague: It hides away when resources are released, thus killing readability. Solution? deinit() or free() functions and (ideally) some sort of defer mechanism. This removes the need for a Drop trait and c++ deconstructurs.
  • Ownership is complicated (probably skill issue), especially when you dive into lifetimes (also probably skill issue): Lifetime of an object stored by value on the stack is ultimately bound to its call stack, if I need it to outlive the current scope I just copy its value out of it, right? Lifetime of a pointer’s value is ultimately bound to it’s presence on the heap (is it?) which without any Drop trait or implicit deconstructor calls is ultimately bound to a deinit()-like call. So if I get the deinit() call right, I get lifetimes right. To me, this seems much easier to understand.
  • Borrowing: I can see why they thought about it, but I don’t get why it’s actually used. The compiler should (should it?) implement guardrails for accessing references, especially mutable one and especially in shared resources contexts. But is borrowing really the solution? Just throw some rules in the language that remove this class of problem? My gut says that the tradeoff in complexity isn’t worth it. Because borrowing is ultimately a matter of lifetime: If the above points hold, then borrowing becomes superfluous. If I just handle my lifetimes correctly (i.e. my resource release and object deallocation), get locks when I need to change something and end of the story. Also I believe this forces programmers to really understand what’s going on. E.g. I’m taking a parallel programming class right now and though I already used threads primitives, I was surprised to see that in reality, I didn’t have any clue about how parallel programming really works at the kernel and CPU level, and now this enables me to write way better parallel programs, because I actually understand what the OS is going to do.

This takes me to another point that’s really out of scope, but: I did an internship in a firm that has around 50k daily users on their platform, sometimes all at the same time, and I don’t think that any of the people that I worked with knew any of what I’m studying in my parallel programming class. Not only that, but I strongly believe that out of the 6 people that work there, there’s not one that understands anything that goes deeper than setting up a JS/php framework, writing some HTML and some CSS (maybe not even that). I find this to be incredibly disturbing: How can someone serve 50k users and not know anything below the framework (not even the actual language, because probably they don’t understand that either) they use?

The results are then pretty clear: You end up with half-cooked software that does the job in double the time it’d actually need to, and end up spending thousands of dollars in monthly server costs just because you can’t program, because let’s be honest, if you don’t know anything about the actual computer (literally the execution mean of your work), or if you know the bare minimum to get work done, are you really an engineer, or are you a programming-bricklayer? And not only that: you lay the bricks, and then you have to remake the whole house every few years because new-shiney-framework.js came up, or the house just sets itself on fire because “react-native super flexible write once run everywhere” application broke because of an NPM dependency fucking everything up.

More in general the more I study this field, the more I can’t understand why do we end up with all this complexity or smart tricks that ultimately result in code that is very difficult to understand and maintain. I don’t understand why anybody would implement four different ways of doing the same thing. Isn’t one that is well-made enough?

Don’t get me wrong, I’m not saying it’s easy: Computer science is a very difficult topic to master, I surely can’t even consider myself a true engineer/computer scientist yet, but I don’t understand why doesn’t nobody asks themselves those question before pouring $100k a year into the hands of crappy programmers that ultimately produce crappy software.

I read Andrew’s Why we can’t have nice software article and I also believe that the above point is correlated to what he says: Why can’t we have nice software? We have to serve the users, but we don’t! We have a very fundamental job in the current world and we should take it as seriously as someone who goes and wants to be a medic. (Almost) Our entire society runs on these programmable machines, and we should be conscious about how we program such machines. We should know better than to ship 1GB application binaries that eat 8GB of ram.

An example of this: A while back I was in an airport and there were machines for automatic passport checks. Out of 15 machines, 10 where broken. The 5 that worked, took somewhere between 5-10 minutes per person. What’s the point of spending money for automation then? I don’t think those machines were cheap to buy nor they add any meaningful value to the users since I still have to wait a ton of time. The best part came when I scanned my passport: The display flashed out a random PowerShell window with some error logs on it. I thought: Why would you ever want to ship an embedded system with a fully fledged windows OS? Why would you even use windows for something like this? It’s a dumb technical decision IMHO.

Since I’m still a student of the arts of computer programming, I’d like to know your opinions about this: Am I missing some pieces? Do my thoughts make (any) sense?

I know it’s a long post and mostly out of this forum’s scope, but I don’t really know where else to ask/talk about this stuff.

Thanks for any answer. I wish y’all a wonderful day.

1 Like

That’s more of a philosophical than technical question, but IMHO if you end up with thousands or tens-of-thousands of individual ownerships and lifetimes in your application that’s already a massive architectural failure, any sort of automatic/enforced memory management is just plastering over this underlying problem - and once you have that problem, a traditional garbage-collected language might be the better and “care-free” solution, since performance obviously doesn’t seem to matter with such an architecture.

E.g. the freedom you get from a programming language without “enforced” ownership and lifetime tracking is wasted when doing manual memory management on a “per-object-level”. E.g. minimize the problem, and a minimal solution will work just fine :wink:

IMHO restrictive languages like Rust have their place for implementing safe sandboxes, but not necessarily for any program that runs in those sandboxes.

PS: In the end, when performance matters, the hardware-constraints dictate the software architecture and nothing else. Those hardware constraints were radically different in the 1990s when languages like Java, Python or JS were created (the “cpu <=> memory gap” didn’t exist or wasn’t as wide as today). Lower-level and much older languages like C were lucky that their programming model is so simple and ‘unopinionated’ that they could easily adapt to the changing hardware landscape, while higher level languages either required massive investments to remain competitive (e.g. see the JS V8 engine) or became irrelevant (ok Python is still very relevant, but mainly because it is used for ‘scripting/glueing tasks’ where performance really doesn’t matter). Arguably Rust falls into the same ‘massive-investment’ category. It solves a problem that shouldn’t exist in the first place (safely managing individual object ownership and lifetime for tens-of-thousands individual objects) by moving the solution from run-time to compile-time, at the cost of a very restrictive programming model. A similar ‘brute-force solution’ is high-performance memory allocators. Allocator performance only matters when you call alloc/free at a very high frequency (e.g. for tens-of-thousands of short-lived objects), again solving a problem that shouldn’t exist in the first place.

4 Likes

Perhaps that it is not quite so black and white. There are all kinds of variations you’ll encounter in the real world:

  • C++ and Rust programmers who rely on RAII but also thoroughly understand what’s happening in their program and are not being misled by “invisible” allocations.
  • Programmers in GC-based languages such as Java who understand the cost of allocations because they do performance testing and profiling, and who use the profiling information to reduce allocations to acceptable levels for their use case.
  • etc.

The questions sound ‘Rusty’.
Ownership can be a logical structure. A programming language should not limit creativity or even debatable incorrectness. The borrowchecker and lifetimes were my absolute worst nightmare.
Just an opinion :slight_smile: