Why Shared Memory Doesn’t Compose
My job is to maintain a decently large and complex legacy code base, one that has its origins in Borland Pascal running on MS-DOS machines. This code base has seen revisions and systems tacked onto it through the years, including a migration to Windows somewhere in there. Well, not only are we maintaining it but we are actively developing it further, adding new features, squashing ancient bugs, and slowly modernizing it piece by piece. Though, there is one piece that sits at the core of the entire product has become the bane of my existence whenever I need to touch it, and so core to the architecture I can’t cleanly rework it.
Shared Memory, a Memory Mapped File, a bit of memory space that the Windows kernel allows multiple processes to write to and read from. My product consists of two processes, one does the heavy, real-time lifting of the control loop, the other handles REST API calls and proxies control messages to the control process via RPC. Where this pain comes from is the fragile house of cards sharing memory between processes.
This colossal shared memory object contains the working brain of the control process, which is itself full of side-effects, but then we must coordinate with other processes. When a new feature requires a new window to be put in on the third floor of this house of cards, it instills dread in my soul. Complexity jumps by default because just moving things around to fit the new window introduces a host of unknown issues. Coordinating pointers, locks, and making sure we’re not using any dynamic size types, or at least if we are those become pointers that live inside the shared memory and that you don’t accidentally use any local pointers; remembering to actually put the information into the shared memory pointers, a nightmare plethora of things to keep track of.
This is where the real problem lies, Shared Memory hides the interfaces between processes behind vague timing and ordering needs that must be perfect or the system fails. If I lock at the wrong time, or get the data structures just out of order from how the other side expects it to be, we’ve got a hidden bug that won’t surface until we exercise the process.
Back in the DOS days, where data copy was expensive and coordination was cheap, mostly thanks to the single process OS it was running on, shared memory made perfect sense. Getting processes that ran serially with things like Borland SideKick to cooperate was nearly free. With modern CPUs, and the advent of preemptive scheduling in modern OSes, that premise has been flipped on it’s head; data copy is cheap, the CPU is no longer as much of a bottleneck as it was; coordination across processes when the scheduler can evict your work at any time to prioritize another process; all of these things make cross-process coordination significantly harder to maintain with the same primitives that were invented in the age of single-threads.
The real killer of Shared Memory is that it fundamentally destroys composability. I can test both of my applications as much as I want, but together, sharing memory, that is an interface that cannot be tested making the whole system just that much less predictable. At my job when I make changes to Shared Memory, I do 10x the due-diligence that it works on my system in my test environment, but there is no guarantee of that behavior. I end up passing off my changes to QA with a tinge in my brain that I am not 100% certain that nothing was adversely affected.
So, before you reach for shared memory in a project, ask what you’re really optimizing for; the optimization shared memory was built for hasn’t been the bottleneck since the 486. What you’re paying for now is coordination, and you’re paying it invisibly.