Paul Hsieh wrote:
... snip ...
You obviously don't realize that the fundamental C technique of
'taking addresses' and 'forming pointers' to things in various
types of storage makes such memory checking totally impossible.
What the hell is your problem? You know very well that I am a supreme
expert about such things.
Unless you are willing to give up all efficiency by making all
pointers indirect, and keeping a master list, and modifying that on
every *p++ coding.
When you are in a corrupted state, indeed there is nothing you can do
that is *guaranteed*. But there is a lot you can do with 99.99%
certainty. This is the whole premise behind the very concept of a
debugger, for example.
My point is that it is very typical that debuggers which are too
general, have very little insight into the state and structure of
memory. Yet, with some work, its easily possible to make a very
useful self-checking memory manager that takes very little performance
and very little extra space overhead. Having written a heap manager
yourself, you must be very well aware of this -- it costs basically
nothing to have a very rich debugging environment for memory.
For example, its trivial to track the base/system heap memory regions
that your manager allocates from. So writing a function like:
int isInAllocatedMemoryRange (void * ptr, int size);
is both useful and easy to do, and *you* of all people ought to know
that you can do this. Of course its not a guarantee that the pointer
is in the right format, or aligned properly, or pointing at live
allocated memory. So we can try to add the following:
int isValidAllocatedMemoryBase (void * ptr);
which would first make sure the allocation was in range, then check
the pointer format, then check the headers for a valid allocation
pattern. Of course this isn't guaranteed to be correct, because you
might be pointing to the middle of an allocation which happens to have
a coincident pattern. But saying this is ineffective, is like saying
the SHA2 secure hash function doesn't work because there might be
collisions. There is a *theoretical* sense in which that's true, but
not a practical one. So we continue:
size_t sizeOfMemoryAllocation (void * ptr);
While one might imagine a heap design where the above might not be
obtainable, this is marginal, and a default of 0 for systems that
can't compute this is not fatal, and could be written into the
specification.
Ok so what next? Well, the headers have to be there for live
allocations, but the contents of free-entries doesn't really matter,
and so is overloaded with data that is convenient for doing free-entry
look-up right? (That's how I did it anyways -- I just do a linked
list, but in general it could encode a B+ tree, to guarantee O(ln(n))
worst case look-up time while always returning allocations when it is
possible.) That means its fairly straight forward to support a
complete heap walking mechanism with no additional overhead for
allocated memory:
int memAllocationsIterate (void (* cb) (void * ctx, void * ptr),
void * ctx);
Handing pointers to free entries to the callback probably doesn't make
any sense outside of system specific purposes, so its just as well to
skip them. Also for multithreading reasons, it doesn't make sense to
make a per-allocation iterator, but instead demanding that the whole
for-loop be encoded in the library under one lock is the soundest way
to go. A heap design which cannot support this could just return with
some value indicating that this is not supported. We can see also,
that this is an alternate way of implementing the function
isValidAllocatedMemoryBase () (nobody says this stuff has to be fast;
remember what we are trying to do and what we are competing against).
This function has a very serious point to it, of course. If you've
corrupted your heap, this iterator is almost certainly not going to
complete and has a very high likelihood of detecting the corruption
(even if the corruption happens in the free-list) and can typically do
so long before your program crashes if used strategically. So using
binary search, you can typically narrow down when any deterministic
program has corrupted its own heap with *near* certainty.
Now, the C method of malloc/free, has generally higher amortized
performance versus garbage collection. But this is typically a non-
issue, since the true memory related speed usually has to do with the
speed at which these allocations are *filled in*, not how fast they
are managed. While alignment is the main way in which C tries to deal
with this, there is no advantage over GC here. Where the *real*
advantage is, is in the function *realloc()*. The problem is that
when it moves the memory and when it just expands the allocation
cannot be determined beforehand by the application. In the case of
resizable arrays, this is particularly infuriating, since the resize
length is typically heuristic but the real cost of doing resizing can
easily be limited by the realloc move performance. So let's suppose
we had:
size_t memExpand (void * ptr, size_t minSize, size_t maxSize);
which will only perform a realloc if the pointer is a valid heap
pointer and stays in the same place, and it can be resized to a length
of *at least* minSize, and maxSize >= minSize. Otherwise 0 is
returned and nothing happens. The function will try to reallocate the
size to as large as possible up to a length of no more than maxSize.
So for an array resize, let us say that we want to write to arr[n+1],
but we've only allocated space for n items. We want to resize it to
2*n to decrease the overhead of future resize calls, however we don't
want to incurr the cost of a memory move if we don't have to. So we
do this:
if (n*sizeof (*arr) >= mlen) {
if (0 == (mlenp = memExpand (arr, (n+1)*sizeof (*arr), mlen +
mlen))) {
void * arrp = realloc (arr, mlen + mlen); /* Will likely
memmove */
if (NULL == arrp) return OUT_OF_MEMORY;
arr = arrp;
mlen += mlen;
} else {
mlen = mlenp;
}
}
writeTo (&arr[n+1]);
So the point of all this is that the high cost of reallocs still are
limited to at most lg2(sizeof(size_t)) actual memcpy()s while
postponing the last large allocation as much as possible and in many
cases avoiding it rather than incurring the cost of one extra memcpy
at the peak size (when the cost is highest). And obviously if a
system can't deal with this, just return 0 all the time. The GC
people don't even play in this arena, but they would ordinarily
counter by saying that realloc has unpredictable performance (since it
might memcpy() with arbitrarily high probability). The above shows a
way of maximally negotiating this performance cost and in real world
code the performance advantage over GC gets widened.
Then finally, its totally trivial to implement the following:
size_t memTotalAllocations (void);
Which would just keep track of either the total memory allocation
requested or the amount committed. Because of alignment adjustment
and the semantics of realloc, it seems that tracking the amount of
*committed* memory is easier. I.e.:
int main () {
void * p = malloc (SIZE_1);
void * r = realloc (p, SIZE_2);
free (r ? r : p);
return memTotalAllocation ();
}
(ignoring the ANSI issues for the moment; its just an example) would
always return 0. So its not hard to see how using this will help
developers track down memory leaks.
In any event, one can see that if such a set of functions were added
to the standard it would radically improve the language's ability,
power, real world safety and usability drammatically. The people who
endorse GC languages would have absolutely no response to these
extensions. And the capabilities would go a long way to cutting the
legs out from underneath the argument that C's memory management is
too unusuable.
One can also see how the whole "why don't you write up a proposal"
nonsense is such a strawman. This stuff practically just rolls of the
tongue. Everyone has seen at least some of these in the extensions of
their compilers, and this is not the first time I or others have
mentioned any of these ideas here.
gets has been declared obsolete for the next standard.
Well, deprecated anyways. It seems that excoriating them for their
nonsensical rationale was at least somewhat effective. Of course none
of this matters if nobody adopts the standard.
[...] There is no
reason to treat strtok in the same manner. strtok can be used
safely. There is no reason that vendors should not supply
re-entrant versions of most of the standard library content.
So I assume that you oppose TR 24731 then? (I do for other reasons,
but I think all their re-entrant function replacements should be
accepted.) If so, then you do not speak for or with the committee as
they seem to be in the process of approving it, in which case these
comments are not really directed at you.