Settings

Theme

Handles are the better pointers (2018)

floooh.github.io

40 points by vinhnx · 21 comments

Reader

11 threads
boricj

That's basically what I've done at work on my embedded project.

Object pools, with indices and generations. Way, way easier than meddling with raw pointers, cheaper than shared_ptr/weak_ptr and no clean-up required when deleting stuff because the stored generation won't match the new object's generation. You can even make that default-constructible if you increment the generation on instantiation too (so that only even generations are alive) and hide it all behind high-level abstractions with C++.

Though my knowledge of these techniques come from 2000s video game architectures, as games like GTA: Vice City are known to use it.

nickwanninger

My PhD thesis is on this topic, and I had an ASPLOS paper on handle based memory support in unmanaged languages (C/C++) through LLVM a few years ago: https://nickw.io/papers/asplos24-alaska.pdf

The overhead we saw there is suprisingly just from instruction and branch overhead from using software handles - I have ongoing work to do it in hardware and the overheads are much lower.

Conscat

I made a real attempt to program this way in my C++ project (largely due to this article), and personally I found it extremely cumbersome because you have to carry the owner of this resource everywhere you want to use the handle. That itself can worsen the performance when it's in different functions, it made generic programming hard and such code has to be more template heavy, and it was difficult to refactor.

But this was a very low level project, and I suspect Floooh was probably thinking more of end-user applications.

michaelcampbell

This article reminds me (on the surface, this is deeper than my experience) of how the recent Goose language orients things.

I never worked with it but I seem to recall the old original Mac API's (Pascal based) used "handles" a lot to solve the issue of being able for the system to move allocations and memory out from under a program without it knowing, as a "pointer to pointer to <thing>". I was poring over those manuals in ~1988/1989 with visions of being a full-time Mac developer in my head.

  • jsrcout

    Your memory is correct.

    The early Macintoshes used the Motorola MC68000 CPU, which lacked an MMU. (Not sure if external MMUs were available then, but in any case Macs didn't have one). And the onboard memory was tiny even for the time (128_K_ for the smaller models).

    So the memory management user API was designed to let the system move memory blocks around at will. "Resources" (discrete data blocks such as menu definitions, icons, even code segments, all with static content IIRC) could even be freed entirely, then reloaded from floppy disk when needed again.

    To make this work, your code would lock a handle before dereferencing it into a pointer to the memory block, then unlock it when finished accessing it. As an application developer it didn't really feel like a burden or hassle, just another minor thing to remember, and it did the job.

    I have my doubts as to how this approach would scale to the larger and more complex software of today, but it would be an interesting experiment.

    https://en.wikipedia.org/wiki/Classic_Mac_OS_memory_manageme...

    EDIT: The Mac's memory API was a bit more complex than I remembered. But it was all part of being a Mac developer back then - everything on the Mac was new and different. For many of us, it felt like the future had arrived. (Maybe not so much the case for those coming from LISP machines and the like).

    https://archive.org/details/inside-macintosh-1992-1994/1992-...

    • michaelcampbell

      thanks for the link(s); "Inside Macintosh" was indeed the set of books/folders I had. Shortly after I bought those, I moved from one state to another and of course the box I packed those in was the one box that got lost in the move =(

emtel

I’m not sure how applicable this is today. It is quite hard to beat jemalloc, especially for small allocations, which it handles essentially the same way a hand coded pool or arena would anyway.

  • throw-qqqqq

    If using 32bit handles, you save 4 bytes per reference compared to pointers. If your application uses a lot of references, this can add up (in total memory use).

    I have a few applications with lots of tree structures, where this makes a meaningful difference.

    • lomereiter

      It's not just memory savings, it's also about CPU cache. A frequently accessed tree/graph-like data structure can fit into L2/L3 cache so much easier if it uses 32-bit offsets instead of pointers.

  • ex-aws-dude

    I’ve worked on game engines and what he’s describing is still very common

    I don’t know about outside that world though

    • ex-aws-dude

      Also something not discussed is that you kind of want handles anyway for serializing objects that reference eachother

throwitaway222

I remember back in the day in Windows C++ programming everything was a HANDLE memory is probably not serving me, but they were all really just void* renamed. And you use them with various win32 functions I guess to keep things straight. I don't miss those days.

  • delta_p_delta_x

    > but they were all really just void* renamed

    The good thing is that all of these can and have been quite neatly put into RAII[1] or some memory arena with custom deleters which use the Win32 functions. One could manually `delete` those handles, but there be dragons. Vulkan does something similar; everything is a VkSomething handle. They may just be pointers directly exposed by the driver, but even so, users are explicitly supposed to use vkDeleteSomething functions to delete them.

    [1]: https://github.com/microsoft/wil

  • andrewshadura

    Famously Win16 API required locking memory allocations to be able to access them by a pointer. Unlocked allocations could be moved by the OS.

program_whiz

there are serious downsides to this, its a tradeoff and I wouldn't say they are "better pointers". There are definitely cases where it can work (e.g. uniform objects that either rapidly create/destroy or all die at a boundary). This is usually the case with a "simple memory model" domain like games, rendering, etc. All the work starts and ends at a request, frame, or scene.

But for anything else, handles can be trouble.

First, you're obviously paying indirection tax on EVERY lookup. Logic like `x->foo` becomes `memory_allocator.objects[x->index]->foo`. You're also paying the generation counter tax (assuming you want any kind of safety). So you also have to do something like this:

    if (x->index < 0 || x->index > memory_allocator.objects.size()) { // panic? }
    auto *ref = memory_allocator.objects[x->index];
    if (!ref) { // panic? }
    if (ref->generation != x->generation) { /// panic? }
    return *ref;
Next, you have type bloat. Hopefully you are making type-safe versions of all these indices so you can't just use an aribtrary index into any kind of array right? Ok well that means now you have double the types (one index type per pointer type). Ok maybe not the worst, just a lot of boilerplate everywhere. Prepare for a lot of this:

   struct Handle { uint64_t index; uint64_t generation }
   typedef PlayerHandle Handle;
Handles are also a lot bigger. If you're memory constrained, adding an additional 16-32 bytes per pointer allocation is painful. For my programming lang I have a single test file that contains 300K AST nodes, that would be another 9.6MB to parse a single file for handles alone, for noticeably slower performance (300K+ indirections and cache misses).

Next, you have the issue of resizing your memory array. You now are in "write your own garbage collector" land. You want to consolidate 50% of the memory that is suddenly free? Well you need to change every index to point the new compacted range (expensive and complex), or manage a free list which potentially increases the memory requirement, and adds complexity, cache coherence issues, etc. Do your memory lists grow indefinitely? Do you need to think about having separate lists for short lived and long lived (generational GC style)? Managing lifespan of objects all sharing one backing storage when the indexes are just integers strewn about is actually _harder_ than doing the same with pointers (just use compacting GC already).

Then there's the issue of object pools. You want your data to live in arrays. Do you put all objects into one master array? You lose cache coherence / iteration speed, and type safety (unless you really bloat and start adding type info to every entry). If you use different arrays for each you might end up with dozens of arrays, one per type, and also their free lists. Now you need a way to manage the lifespans and scope of all these object pools. Suddenly, you've reinvented malloc / free and segfault signals.

Or you could just wisely use pointers...

Surac

my API always use handels, not because i design them that way, but in every iteration they get more and more handles

mcphage

This sounds a lot like ECS—or at least, how ECS systems handle memory for the components.

FLeXMurphy

They always have been, and Win32 extensively uses HANDLE with some underlying restrictions. I remember reading up that certain numeric allocation buckets in HANDLE corresponded to the type of underlying resource (ie. Nth bits mean <this resource>), but it has been too long to remember the details.

PaulHoule

A nice analysis of large-scale programming techniques that go beyond the novice version of "OO Sux" to the almost secret

https://en.wikipedia.org/wiki/AoS_and_SoA

and why game engines use entity-component systems, analytical databases are column-oriented, etc. That is, the classical organization of "objects" as structures has a lot of overhead when you're doing heavy work. For instance java reifies objects with a big header, a monitor lock, and other stuff that really adds up if you, say, are writing a chess engine and need to maintain transposition tables. And even if you are in C++ and there is a lot less overhead, AoS still has bad mechanical sympathy compared to SoA.

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection