Actually, I think the best here, in this discussion, would be to try and forget the *heap* and not even call memory available for allocation a "heap". Or even several of them. That would avoid being biased by what we are used to.
Right. In that case, let me rephrase: the pattern where I do use reallocation is very distinct.
- Do you typically use reallocation for growing blocks, or for both growing and shrinking? (My own use cases of realloc() are almost always only 'growing'.)
Typical cases are buffers for things like files that fit in memory. I do not use the pattern where one examines the file size, and then reads that amount of data, because it is unreliable. Instead, I read into a dynamically growing buffer, and when complete, (do a whitespace compaction, comment removal pass and then) shrink the allocation to the size used. The shrinking is useful if additional allocations are done before the file area is freed, for example for key data needed later.
When splitting one into separate chunks, the possibility of a chunk ending in the middle of a multibyte identifier (say, escape sequence, or between CR and LF in a file that contains CR LF newlines) is the detail that makes handling them in chunks annoying. The simple approaches are "slow", and the fast approach (a true FSM that can be progressed byte by byte) complex or hard to maintain.
In the getline() pattern, the buffer may grow, but isn't shrunk; it will eventually be freed. I do not want to pre-allocate a buffer that can contain the largest possible line (buffer) I want to support, because that occurs too rarely. As an example, most C source lines are short (say, under 200 characters), but occasionally you have generated code etc. that can have lines with thousands of characters.
- Is that typically to implement dynamic tables?
No; the common denominator seems to be
human readable text or a related format, like config files, JSON, HPGL, G-code, etc., and input of unknown length or complexity.
I often use a binary heap for timeouts, with the heap represented as an array of (time, event_id) pairs. Each event_id refers to a slot in a separate array, that contains the timeout state and an offset back to the entry in the heap. Percolation is only a bit more complex than in an ordinary binary heap, since also the reference back to the hash table needs to be updated when an entry moves in the heap. That makes it cheap to delete any entry using just the event_id. The slots can be split into fixed-size chunks trivially, but the heap really does need to be contiguous in memory.
However, when the heap array needs to grow, the old one can be freed and a new one allocated from scratch, because the new one can be populated in a single pass over the active slots. It can be
easier to use realloc instead, but I don't think I really need realloc() to implement a timeout heap efficiently.
For hash tables, I use malloc()+copy+free(), not realloc().
Images etc. specify their size before the data is read, unlike text/stream formats, so no realloc() needed there either.
An interesting detail: Current implementations for parsing floating-point numbers are slow. Even on spinny HDDs, the parser tends to be the bottleneck, not the storage I/O speed. Daniel Lemire's
Number Parsing at a Gigabyte per Second describes the issues and approaches well, but even it gives up and uses the old arbitrary precision approaches for values with more than 19 digits in the significand (decimal part excluding the power of ten exponent). (Arbitrary/multiprecision support does not need realloc(), because the new size is known when summing or multiplying anyway.)
While this might sound irrelevant to embedded stuff (since HPGL and G-Code limit real parameters to a smaller range and precision anyway), your JSON and configuration data can contain decimal-format floating-point values, and the slowness (and memory needs) when parsing these can be surprising. Speccing max. 19 decimal digits in the significand, and rejecting the input if more are given (or just using a best guess approximation), can make a big difference in both speed and firmware size.
When I do use realloc(), I almost always have only one "active" region I realloc(); it is extremely rare to need more than one such chunk of memory at the same time.
I often do do "normal" malloc()s at the same time, with varying life times, but these never get realloc()'d.
Perhaps the best description is that on top of my normal malloc()+free() patterns, I sometimes need a separate buffer whose maximum size should depend on the available memory, but should not preclude making normal allocations. I can even live with this buffer being moved or resized when making those separate allocations; no problem.
This is why I suggested that maybe a separate interface for this, where the user code specifies the size it currently uses and may modify it at any time, but the allocator dictates its maximum size and may move and resize it to satisfy a normal allocation done at the same time.