Not that you can do much when malloc fails...
Not that you can do much when malloc fails...
Tell the user they can’t do that operation? Use on-disk storage instead? Re-use memory you already have (such as evicting a cache and taking the memory it already had allocated)? Abandon what you were doing if it was only an optimisation and wasn’t essential (such as allocating memory as part of a speculative execution)? Run a garbage collector and try again?
Lots of options available in some situations.
Once 32bit hardware-based virtual memory entered the picture, every single application would just assume you have endless RAM - memory checks are reserved for "common cases" like trying to create a 100000x100000 image in a (32bit) image editor.
The reason for all that is simple: out of memory situations are stupidly and increasingly rare and in most cases where they can happen there isn't much you can do (e.g. what would you do if you run out of memory while making the fourth button in a toolbar?) and really in the 99% of the cases there might only be five people in the entire universe that will encounter such a case (two of them will tweet about it though and amass a lot of "lol, those garbage developers" retweets) so littering your codebase to keep those five people (and their Twitter followers) happy is not worth the effort. I mean, are you really going to put a "run out of memory" check after every toolbar button allocation? And what are you going to do if that fails? What can you do?
AFAIK some modern languages nowadays even assume memory allocations wont fail (and if they do they just terminate).
Rollback what I’d created so far, evict caches, ask malloc to trim, try again, if that failed roll back again and ask the user to reduce the volume of application data open by closing views or documents or whatever before they try again.
But yeah it’s a lot of engineering.
Silly engineering like what you suggest is a waste of time. Non-critical applications that are allowed to crash (every normal desktop app) would end up overengineered and unmaintainable. Embedded systems that must give guarantees about their reliability simplify system design drastically. One of the things going out the window first is dynamic memory allocation. It has too many failure modes.
You just failed to create a toolbar button, how are you going to ask a user do something if you already failed to create a tiny UI element?
So the typical behavior you saw was a combined effect of physical constraints and lack of skill or care.
Exiting cleanly would mean trapping SIGBUS/SIGSEGV (can't remember which one is invoked) and being able to roll-back from just about any point in your code or libraries)
You need to ensure overcommit is disabled, but because that's a system-wide setting, your program can't easily force that on.
I have seen code hit an allocation failure, bubble the error up the stack, which causes various pieces of code along the stack to free their heap allocations, all the while the process is able to log what is going and keep running.
I have even seen this happen when the allocation that failed was tiny, less than 100 bytes.
I don’t know, maybe this is old school, but yes you should put a check on every toolbar allocation for two reason: one you can gracefully report why your app isn’t doing it’s job and two it is a very very slippery slope when you start ignoring those errors as a matter of practice
What if you cannot do that report because the reporting itself needs memory that can fail?
> but yes you should put a check on every toolbar allocation for two reason: one you can gracefully report why your app isn’t doing it’s job
This is a reason to put checks in every toolbar allocation, but on the other hand there is also a reason to not do that: you are trying to catch a case that will only happen at a 0.00001% of the time yet to do that you are making the codebase more complex which will affect working with it 100% of the time.
Unless you are working in something like a medical or nuclear device, it is not worth to bother with such things. Even then when it comes to high risk programs relying on the programmer doing things right outside the core functionality is probably (i haven't worked on such projects myself so i'm guessing here) not a good idea and instead you should compartmentalize (e.g. the core functionality is running as a separate process from the UI and if the UI crashes, a watchdog restarts the UI process). But that is my guess here, i mainly have common end user/consumer applications in mind.
Where "something like a medical or nuclear device" implies a piece of software that is expected to work correctly.
In other words, you should always ensure that failures resulting from malloc(3) returning an error (and all other errors) are suitably contained.
My implication was software that if it fails it will kill people.
Otherwise if a program crashes because of a situation happens once every 100000000 runs, not only is worthless to worry about it, it actually is preferable to not do that as to keep the codebase clean and hence easier to maintain for bugs that actually do affect people.
Allocate and reserve error-reporting memory early. For example, your ErrorReporter instance is initialized at start-up, and doesn't call malloc() when you make a report.
Allocate a big block of memory at startup. Free it just before doing the reporting.
If the out-of-memory was caused by something simple like accidentally loading 2G of data because of some odd data in an network request or a user selected file, and it rolls back to the start of the UI action or the incoming network request, the application may still work fine.
(I usually connected other out-of-resource conditions such as pipe/socket/open failures to std::bad_alloc too. They're pretty comparable in effect and give a bigger chance of actually exercising those exception paths)
If you have a system that handles independent requests, it might be more robust to model each request as its own process, and then simply allow those processes to crash if they find themselves in an unexpected condition (and handle process crashes).
In eg an application server, requests themselves may be independent, but still share a lot of cached data - and as long as locking overhead doesn't overwhelm you, a single process is still the fastest way to share data between (worker) threads.
If your threads need to share interleaved modifications to some common state then that approach isn't viable. But in that case it will be very hard to "roll back"/"skip over" a failed request, as it's difficult to be confident that you haven't corrupted that shared data in a way that will cause the same problem for future requests.
I'm not sure why skipping over failed requests is hard. VMs associated with the request are aborted, the client gets a 500/503 and can retry later. Incomplete data doesn't get added to the caches at all, so no corruption of shared data.