Ownership, borrowing, lifetimes, parallel programming and unnecessary complexity

that’s exactly what i meant by

Zig says “comptime evaluation will halt because I’ll halt it”,

yes :slight_smile:

1 Like

In the 80s and 90s, the standard was to pre-reserve a buffer with a fixed size.
It had several advantages:

  • Fixed upper bound enforces a strict limit on memory usage
  • Only need to allocate and free once, or in some cases not at all
  • The memory never leaks

They didn’t need garbage-collection or reference-counting, since they already knew that the memory would only be freed under very specific circumstances (program exit, usually).

Of course, this allowed very easy access of undefined memory by indexing the buffer wrong, which is why 80s and 90s programs are notorious for being exploitable in spectacular ways, but it was objectively the most performant way for developers to use their very limited memory.

In fact, the original DOOM always reserved 8 MB on the heap on program startup, and then used its own system to internally manage that memory, like a FixedBufferAllocator in Zig. The result is that after the program is initialised, it never, ever needs to allocate memory through syscalls again.

The glaring issue is that, of course, you might reserve more memory than you actually end up needing. Or more memory than the user has!

4 Likes

For database caches (what I’m working on) this is still the common approach. The fixed size is fine because it’s a cache – you decide up front how much memory to give the db and when it fills the least recently/frequently accessed items are evicted.

On the safety issue, this is solved in my case by never storing pointers in the cache, just plain-old-data. A bug can cause a data race, but not a memory safety issue.

Apparently TigerBeetle (also a database, implemented in Zig) uses fixed size allocations for everything. I’m not a OS person but I assume OS implementations also use a lot of fixed size data structures. It’s still a good tool to use whenever you can.

3 Likes

Regarding your more general feelings, yes, there absolutely are a lot of programmers who don’t know anything below the framework layer. Whether that’s a problem, though, depends on who you ask. It’s certainly working fine for the programmers and the companies they work for - even if they could hypothetically squeeze more performance and better software overall out of lower-level engineers, I don’t think they’d consider it cost-effective to do so.

Perhaps it’s better that systems-minded people direct their energy to systems where that makes the biggest impact? I don’t know.

I do think it would be unfair to discount the skills of experts just because their domain is higher level and not as concerned with performance, though. Sometimes worrying about teamwork (I know, the old “oop is good for large teams” argument, but it seems to be working well enough in a lot of spaces regardless of our personal feelings), modularity, correctness, or iteration speed is simply higher priority.

2 Likes

That makes sense that this was all common knowledge in the 80s. I had a sense that OSes worked that way but my experience was to start off focused on RAII C++ in the 90s then onto GC C# and then failing in my attempts to master C as the cross-platform lingua franca missed all that - probably distracted by always shooting myself in the foot.

What I’m getting at here is that this is the first time I’ve really been able to see all this so clearly and I totally lack the real historical insight there, so thanks. :slight_smile:

1 Like

The syscalls probably aren’t really an issue if one uses something like the PageAllocator as the backend for another Allocator, but with eg 64 pages at a time (256k blocks instead of 4k).

But for multi-threading, you still have synchronization overhead for the internal allocation and free routines.

Was the original Doom multi-threaded? Idk, but I guess not, given the typical CPUs of the time.

1 Like

Nope, not multithreaded. You can absolutely use pre-allocated blocks of memory in multithreaded code, though. Just be careful and methodical about how you chop it up and interact with it.

It could be, yes. In practice it doesn’t handle mutual recursion correctly, so it runs for an unbounded amount of time and overflows the program stack.

Which is a sort of halting. If you squint.

1 Like

Squinting, I see an implicit unpredictable hardware/OS-dependent stack usage quota.

Indeed! Joel David Hamkins said exactly that in a recent interview with Lex Fridman.

Fortunately squinting is my favorite thing to do when writing mathematical proves :slight_smile: (presumably to the dismay of my professors…)

In the 80s and 90s, the standard was to pre-reserve a buffer with a fixed size.
It had several advantages:

FWIW I use this approach (pre-allocated global state) to a bigger or lesser degree in most of my projects.

For instance this Pacman toy project has all state in a single static global:

I had to ‘rediscover’ this technique after nearly two decades of OOP/C++ cool-aid consumption.

…likewise in my home computer emulators, all state is in a single state struct (including the emulator memory and ROM dumps), and there I’m actually using this for a ‘poor man’s snapshotting’, I can simply store and load this struct to disk to create a cycle-accurate emulator snapshot (the snapshots break of course after changing the struct layout):

PS: the ‘missing language feature’ for this “all state is in a single global” philosophy in bigger codebases is a filesystem-like permission system (restrict read/write permissions for specific parts of the code to specific parts of the data).

1 Like

I think you could get close with a horrible abuse of C++ singletons and friend classes.

Note that I said could not should. I dislike myself for even thinking it.

4 Likes

You will get quite different answers in a Rust forum. To avoid bias and skew, I suggest that you do that as well. One important consideration is guarantees. For instance, with defer there is no guarantee that it won’t be omitted or scoped incorrectly or used when it shouldn’t be–these errors are common. Zig and its community favor manual and freedom, Rust goes the other way.

Note that there are languages that try to infer/automate borrowing and lifetimes–see, e.g.,

Memory Management in Lobster

and

Loon — A LISP that flies

The undecidability of the halting problem only applies to the set of Turing Machines. There’s an infinity of less computationally powerful mechanisms for which halting can be proven … one example not really logically different from having a branch quota is the singleton set of machines that have “halt” as their only operation.

1 Like

The glaring issue with fixed size buffers is buffer overflow.

1 Like

Not today. Buffers are represented as slices these days.