BIND 10 #2088: introduce MemorySegment class
BIND 10 Development
do-not-reply at isc.org
Mon Jul 9 23:33:06 UTC 2012
#2088: introduce MemorySegment class
-------------------------------------+-------------------------------------
Reporter: | Owner: muks
jinmei | Status: reviewing
Type: task | Milestone:
Priority: | Sprint-20120717
medium | Resolution:
Component: data | Sensitive: 0
source | Sub-Project: DNS
Keywords: | Estimated Difficulty: 4
Defect Severity: N/A | Total Hours: 0
Feature Depending on Ticket: |
scalable inmemory |
Add Hours to Ticket: 0 |
Internal?: 0 |
-------------------------------------+-------------------------------------
Comment (by jinmei):
Replying to [comment:10 muks]:
> Hi Jinmei
>
> Replying to [comment:9 jinmei]:
> > Some quick comments:
> >
> > - I myself am not really sure, but I guess we'd probably like allocate
> > to throw when it fails to allocate the memory.
>
> "Fails to allocate memory" depends on the implementation, so whether it
would throw or not would depend on the implementation.
First, let me clarify I actually meant the case where malloc() returns
NULL by "fail to allocate memory".
> For the `malloc()` implementation, `malloc()` could return NULL. There
are arguments both for and against NULL-testing this pointer, and I side
with not NULL testing it unless we think it's reasonable for it to fail
(such as for very large buffers). For small sizes if it returns NULL, in
typical programs it is a fatal situation. Throwing here isn't likely to
help and could have issues of its own. If large sizes are requested, then
it's better left to the caller to handle a NULL return. For a well-written
program running on a system reasonably configured for it, there should not
be an unexpected out-of-memory issue in `malloc()`. If there is, it would
be due to some programming (leak) or system configuration error.
I tend to agree that when malloc() returns NULL for a reasonable size
it would be leak or some system level error. But for the rest of the
above I either don't understand it or disagree on it.
I don't know what you mean by issues of throwing itself, but our
overall implementation generally expects when the free store operator
'new' fails to allocate memory bad_alloc is thrown (although we
generally only catch it at the highest level of the application and
exit immediately). Since MemorySegment::allocate() is a kind of
wrapper for the standard memory allocator, I think it provides more
consistent interface.
I'm not sure what you mean by 'not NULL testing'. The current
implementation in the branch actually does test if the returned value
is NULL or not:
{{{#!cpp
void* ptr = malloc(size);
if (ptr != NULL) {
allocated_size_ += size;
}
}}}
If you mean by 'not NULL testing' we'd let the caller handle the
situation where the pointer is NULL, I'd say it'll just result in the
same behavior (throwing an exception) with forcing the caller to
repeat the same pattern. As noted above, our overall interface
expects memory allocation to generally succeed or result in an
exception in the worst case, so the caller would often have no other
way to handle it than by throwing an exception itself (it's generally
the case for constructors, and also apply to many methods/functions
that return void).
If you actually mean by 'not NULL testing' even the caller doesn't
care if MemorySegment::allocate() returns NULL and it will let the
program subsequently segfault or something, I've never heard of that
argument as a reasonable design option. At least I personally
strongly oppose to it. If the caller immediately tries to dereference
the returned pointer, it will immediately cause a segfault, and the
end result is not so different (I still prefer making it more explicit
via an exception (with which we could handle it a bit more gracefully
if we want) or assert()ing it, but at this level I see there are both
for and against camps). But if it simply stores the pointer somewhere
and returns, basically assuming the pointer is valid, it will create a
strange inconsistent state in the system, which will lead to very
strange crash (if we are relatively lucky) or some awkward behavior
(if we are more unlucky) that is very difficult to debug because the
real cause of the problem is far from where the visible issue happens.
So, to me, the only possible options were
1. MemorySegment::allocate() throws if malloc() returns NULL, or
2. MemorySegment::allocate() does nothing if malloc() returns NULL and
passes it to caller. We request the caller handle that situation
appropriately (in documentation), which would be throwing an
exception in practice anyway.
Originally I was not sure which one is better in this specific case
because in the limited usage of MemorySegment::allocate(), it might be
more convenient for the caller to explicitly catch the rare failure
case (than doing try-catch or using some wrapper class for RAII). But
I now expect it will probably not be the case. Since option #1 is
more consistent with other part of our internal interfaces, I now
prefer this option. But if you meant option #2 for some specific
reason, we can discuss it.
> There is one more issue. In some demand paging kernels (Linux included),
allocations are over committed in the default configuration (unless it's a
very very large buffer requested or some other conditions exist). For
typical `malloc()`s, it will never return NULL. libc requests more memory
and the kernel always allows the `brk()` to pass and creates table
entries. Only when there's a page fault does the kernel try to find a
frame for it and OOMs processes if necessary then. So NULL checking of
`malloc()` output won't be useful.
This doesn't seem to be an "issue" to me, just another incident of
being locked in with a particular implementation. The fact there's an
implementation of malloc() that never returns NULL doesn't mean we
"shouldn't" handle this error case. And, there is in fact other
implementation of malloc() in the real world that can really return
NULL under memory shortage; I encountered that situation with BIND 9 -
if my memory is correct it was Solaris.
> NULL checking could be useful in other MemorySegment implementations.
If we use boost memory segment libraries, they throw by default.
Also, IMO the base class should provide the unified interface on this
point anyway; we shouldn't change the behavior depending on details of
the underlying segment implementation.
> > - what if ptr is NULL in deallocate? What if size is too large?
>
> Deallocating NULL would be a fatal programming mistake, not something
that catching can fix. It's better for the process to segfault and dump
its core then.
First off, free() won't segfault with NULL. So segfault won't
happen with this implementation:
{{{#!cpp
void
MemorySegmentLocal::deallocate(void* ptr, size_t size) {
allocated_size_ -= size;
free(ptr);
}
}}}
Secondly, while I agree that deallocating attempt of NULL pointer is
generally a program error (but not necessarily so - sometimes it could
be a reasonable idiom for making the code simpler by eliminating the
need for an if block), and I also agree that we cannot directly fix
the error by catching it. But it doesn't mean explicitly catching it
doesn't make sense - it helps a possible bug earlier and more
clearly. I personally prefer such stricter checks and the benefit
they provide, but I see this is a case where two camps exist. Now
Jelte seems to become the official reviewer of this branch, I'd leave
it to you two. In either case, I think the expected behavior should
be documented.
> If size is too large, we can probably add a trivial check against the
allocated count. It will only catch larger than allocated count problems.
It could be caught in the system's implementation if the implementation
takes the size of memory to deallocate. Even in the case of `free()`, at
the end we'd have a non-zero allocated count pointing to a bug. But we can
add this check as it'd catch one type of problem.
If size is too large, `allocated_size_` will become an obviously
invalid, very large value. I don't like to leave the object in such a
clearly broken state. Also, if this happens it's quite likely that
caller is passing a pointer that was not really allocated by this
segment. IMO it's better to catch such an error as soon as possible.
But I'd leave the decision on this point to you and Jelte, too.
One other thing: the base class destructor should be declared and
defined explicitly and be made virtual.
--
Ticket URL: <http://bind10.isc.org/ticket/2088#comment:13>
BIND 10 Development <http://bind10.isc.org>
BIND 10 Development
More information about the bind10-tickets
mailing list