Andrew Lentvorski wrote:
> Christopher Smith wrote:
>
>> The fact that you said "C/C++" kind of screams that you aren't getting
>> the point here. C most definitely does not have destructors, and in fact
>> most resource management related bugs in C++ can be traced back to
>> failing to apply C++'s semantics for avoiding these problems. One of
>> C++'s greatest strengths (C compatibility) really turns out to be a
>> short coming in this regard: programmers code C++ programs like they are
>> C programs.
>
>
> Sorry. I misspoke. I tend to think of manual resource management and
> lump C/C++ together.
>
> You are correct that C++ has destructors. And this works
> fantastically well as long as programmers only use the STL and access
> memory.
Nothing magical about the STL.
> Once you touch non-memory resources or have to write your own
> destructors, it breaks down.
>
> You can't put cleanup code for things like sockets into destructors
> since even closing a socket can cause errors. And destructors can't
> throw exceptions. Whoops. Hence, back to manual resource management
> and C++ is no better than Java (it is, in fact, worse but I'll just
> assume equality).
Destructors can, of course, throw exceptions. You just need to
understand the implications of doing so. In particular, in the event
that std::uncaught_exception() returns true (or would return true if you
invoked it), the runtime's terminate handler is going to get called.
This, or the other obvious alternative of ignoring errors when freeing
resources (many resources simply can't fail to be freed anyway), is
actually the desired outcome in most cases. So either plowing ahead or
not throwing an exception works out pretty well most of the time. The
most common bug vis-a-vis freeing resources is failing to try to free
them in the first place, not failing to handle the case where you
couldn't free them.
In the event where you do want to try to notify/recover from a failure
during a destructor (and sockets have been the only case where I've
wanted anything more than logging), it is rare that the unwinding the
stack is going to get you to a context where a notify/recover operation
is going to somehow make more sense. So even if you could, throwing an
exception really doesn't help you. In general, in these contexts you are
in a very messed up state. Having the caller try to patch things up is a
recipe for disaster, as the odds of them getting it right once are long
enough that you know they are going to screw it up in at least some of
the places where the resource is used. Most often just flat out
terminating is the best thing to do (no foul in calling abort() from a
destructor). If not, what you probably want is a policy for handling
these cases that is defined in your framework somewhere. So, you can do
something like:
template <typename CleanupFailurePolicy>
class Socket {
int fd;
// stuff
~Socket() {
if (-1 == ::close(fd))
CleanupFailurePolicy::close_failure(fd, errno);
}
};
Sometimes it makes sense to have an instance of CleanupFailurePolicy so
the failure can be handled in the specific framework context.
Anyway, the notion that it's no better than Java is just silly. Sure,
trying to recover from a failure to free a resource with anything much
more sophisticated than abort() is tricky work regardless of your
programming language, but Java doesn't even provide a mechanism for
encapsulating the logic of when to free up a resource, and the nastiness
of Java code for the simple case of "just try to free up resources as
soon as I no longer need them" is just painful to code up.
> And, if someone has to write their own destructors, there are a bunch
> of guidelines that they have to know cold so as not to get things
> wrong. Pass.
Hey, I never said that it was easy. Doing the right thing with resource
management never seems to be. I'd much rather have to get it right just
once.
>> Garbage collection doesn't manage sockets at all. That's the problem.
>
> And neither can destructors. Sorry. If you don't understand that,
> you're in deep trouble already. Please go read "Exceptional C++" by
> Herb Sutter (Gah! Checking amazon.com he's up to 3 *entire* books on
> the problems of exceptions. Yuk. Glad I don't do C++ anymore ...)
If you'd read the books you'd know that a) they are short for technical
books and b) the title is amusing but misleading. In fact the books are
collections of the Guru of the Week series. They typically have over a
half dozen major topics, only one of which might be related to
exceptions. Frankly, if I were writing a similar book on Java I'd
probably end up devoting a much more to proper use of exceptions,
although it's hard to know how much of that could be attributed to Mr.
Sutter's wisdom.
> Destructors only *look* like they can clean up non-memory resources.
> And that's the real problem with C++ ... so many things *look* like
> they can solve your problem.
It's not about solving problems. These aren't silver bullets. I'm just
looking for the "easy things should be easy, hard things should be
possible" kind of solution. It should be easy to have code that is going
to give it the old college try when it comes to cleaning up resources,
and possible to have code that handles complex algorithms for cleaning
up resources in the event of failures.
> It's only after writing 10,000 lines of code that the subtleties start
> to bite. By then nobody wants to go back and redo the architecture to
> get it right. Been there, done that, got the scars, don't want any more.
Too bad. Refactoring one's code tends to be instructive.
>> There are certainly classes of programs where this stuff doesn't matter,
>> particularly with building a simple prototype. If you are trying to
>> claim that this isn't an issue for a lot of Java programs, check out how
>> man serious projects don't have a "finally" clause in them. ;-)
>
>
> Java does use finalizers as a safety net for sockets:
Yes, and that's all it is: a safety net, and a poor one at that. Unlike
with memory management, when you run out of sockets, there is no
mechanism for implementing a policy to free up unused ones. You are
basically left with having to build a framework which wraps sockets,
tracks open ones with weak references, and then tries to figure out
which ones it can/should close down in the event that you can't
instantiate any more. I've been burned by this one, with specific
reference to sockets, more often than I care to think about.
> And, BTW, "finally" gets used for locking in Java, as well.
I was referring to Java's built in monitors when I mentioned it's
support for managing synchronization resources cleanly. While there is
no failure case for releasing a monitor (I told you it was common ;-),
it is very easy to write code that will release a monitor as soon as it
no longer needs it, and really hard to forget to release a monitor. No
"finally" necessary. The fact that Java is able to do this for specific
resources (memory and its monitors) is a good indicator that it can be
done more generally.
>>> I have the same opinion about the Collections interface in Java. I use
>>> the concrete interface rather than the Collection interface. Use the
>>> richest interface that makes sense.
>>
>>
>> "concrete interface"?
>
>
> I use will use LinkedList (the actual data structure) instead of Queue
> (the collections interface).
>
> Java prefers Queue on the theory that you can change the underlying
> implementation (LinkedList). I'm with Scott Meyers (see Effective
> STL) that this is a red herring. You normally need guarantees about
> performance and complexity (big O() notation) that preclude changing
> the underlying data structure anyway.
I think you are overstating Meyer's position. Yes its important to have
complexity guarantees, but often those guarantees are much less specific
than a given implementation provides you, allowing for a variety of
different algorithms/data structures to get the job done. For example
you might only need the guarantee that the amortized complexity of
enqueuing and dequing be O(1). This is why the STL tends to take
iterators as parameters instead of collections, and then enforces
distinct performance contracts on the different types of iterators.
>>> I do find the fact that I can't create a default action for interfaces
>>> in Java to be annoying.
>>
>>
>> If you use factories and dynamic proxies you can build a framework that
>> will do it for you. You can probably do something similar with
>> attributes as well.
>
>
> "The usage of design patterns indicates a language failure."
>
> I forget where I heard it, but I am now convinced of this. If I have
> to use patterns, I am in the wrong language.
The thinking behind the statement has merit, but the semantics are
horrid. Whether a language has a built in
operator/primitive/class/whatever that addresses the design pattern
trivially or you have to construct the thing from scratch, you are using
the design pattern. Additionally, there are tons of design patterns out
there, and it doesn't make sense for a language to try to address them
all. Even "huge" languages like Common Lisp, Ada and C++ can't even come
close to addressing all of them. That being said, the more common the
usage of a design pattern, perhaps the more important it is that a
language designer should be a trivial way to implement said pattern.
Anyway, I totally agree that it is a failing of Java's to not provide an
easy way to do this, but you often end up using factories with Java
interfaces anyway, so I thought it might be helpful for you to know of a
trick to get the job done in that context.
>> This is a totally different point, but the difference is that in C++ you
>> can allocate objects and arrays on the stack, which really makes a huge
>> difference (so much so that the next Java runtime is going to have
>> escape analysis so that it can finally allocate on the stack... and the
>> performance win from this is huge).
>
>
> While that's useful, allocating from the heap is almost as cheap as
> the stack in Java. What memory pools lose in call efficiency they
> gain in cache/memory locality.
I dunnoh. Stacks have pretty darn good cache/memory locality as well. ;-)
> The primary wins for escape analysis are:
>
> 1) alleviating runtime checks (big one)
> 2) eliding synchronization barriers (this is the major expense in Java
> allocation from the heap--occasionally you have to synchronize your
> heap with other threads)
> 3) tail recursion (this is *huge* for other languages on the JVM)
You missed some others: reduces thrashing of new space (small win) and
older generations (big win) and frees up memory immediately upon exit
from a stack frame, reducing working set size.
> Overall execution time only drops between 2-23% with a median of 7%.
> Nothing to sneeze at, but certainly not "huge".
> (See: http://citeseer.ist.psu.edu/choi99escape.html)
The paper is old. While it is instructive, the performance improvements
shouldn't be considered indicative of what is possible or typical with
Mustang. For starters they were working off of IBM's Research VM and
made different design choices than the Mustang folks made. Secondly,
they were working with hardware much older than today's stuff, which
tends to be far more sensitive to memory issues. The Watson folks
themselves have improved on this work since that paper was written. They
also didn't test the impact this has if some of your virtual address
space is swapped out (well, a couple of tests hit upon this but in
limited ways), and finally their benchmarks tended to be CPU intensive
rather than memory and/or synchronization intensive. In practice with
Mustang I've seen code perform over twice as fast by turning on escape
analysis with the typical improvement being in the 10-30% range, and
I've heard from others who've claimed as much as an order of magnitude
performance improvement. Sure it's nothing compared to algorithmic
improvements, but that's one of the more significant jumps I've been
able to make by just tweaking the runtime.
--Chris
--
[email protected]
http://www.kernel-panic.org/cgi-bin/mailman/listinfo/kplug-lpsg