As I pointed out yesterday, with FastMM available, memory management is so much of a solved problem that it’s a non-problem. So dropping a performance-killing pseudo-GC “solution” on us is patronizing and insulting in the extreme, not to mention a massive waste of effort that could have been spent actually improving the language and/or standard libraries. But I did notice one very interesting thing from its implementation: the introduction of the [Weak] attribute, and a few other related attributes, are something fundamentally new in the Delphi language.
Attributes were introduced to Delphi as part of the extended RTTI package in D2010. They’re essentially little tags you can put on a class (or various other places) that provide metadata about it, which can be retrieved via the RTTI system. But [Weak] is something more: it’s an attribute that has a specific meaning to the compiler itself. It’s not just a metadata attribute, but a semantic attribute. And that concept of semantic attributes, if employed in a slightly different way, could be used to make Delphi code a lot prettier without having to slow everything down, break tons of existing code, and tick off the vast majority of Delphi developers who do not want garbage collection in the language.
Consider the following code:
procedure myProc; var myObj: TMyObject; begin myObj := TMyObject.Create; try myObj.DoWhatever; finally myObj.free; end; end;
You’re probably familiar with this pattern. You’ve probably written code like this a million times. It’s The Right Way to write it: you clean up after yourself once you’re done.
It’s also kind of bulky. The body of the procedure is six lines long. One for setup, one for doing what we actually care about, and four for cleanup. The ARC version would be a lot shorter:
procedure myProc; var myObj: TMyObject; begin myObj := TMyObject.Create; myObj.DoWhatever; end;
But while that may look like less code, there’s actually a lot more going on. That try block is still there; it’s just been added implicitly by the compiler. And instead of calling Free directly, there’s a ref count release call (which slows down everything because it has to use an atomic decrement) before the destructor. And the stupid part is, none of it is necessary. Even the most trivial static analysis could show that this reference to myObj is never going to be passed to anything else, so there’s no need to keep a count of it, at all. A smart compiler should be able to automatically insert a direct call to Free without having to keep a reference count here.
The problem is, the only static analysis that can really do any good is “the most trivial static analysis,” which can only be useful in the most trivial of cases. For example, if the TMyObject constructor passed Self to some routine somewhere, it may or may not have held a reference to it in an object, and then all bets are off. Have you heard of the Halting Problem, how it’s impossible to come up with a general-case analysis routine that will determine if a given program will halt or not? Well, it gets worse. There’s this thing called Rice’s theorem which states that it is impossible to come up with any general-purpose analysis routine that can answer any useful, non-trivial question about computer programs. Which means that there’s no way to write a static analyzer that can detect which variables can be freed safely in a situation like this.
Rice’s theorem says that it’s impossible to create a smart compiler that can take care of something like that, for the same basic reasons that it’s impossible to solve the Halting Problem. Simply put, you can’t automate “smart,” which is why attempts to do so invariably end up with stupid solutions that require dumbing things way down. (See: every implementation of garbage collection ever devised, including ARC.)
This is where semantic attributes come in. With a bit of compiler support, it would be simple to accomplish the same thing, like this:
procedure myProc; var [Local] myObj: TMyObject; begin myObj := TMyObject.Create; myObj.DoWhatever; end;
The semantics here should be intuitively obvious. A [Local] attribute would declare to the compiler that this variable is scoped to the current declaration. Instead of trying to automate “smart”, which is impossible, we keep the smart decisions where they belong–with the intelligent person behind the keyboard–and instead get the compiler to automate something computers are very good at automating: mindless drudgery. The [Local] attribute would transform the procedure above into this:
procedure myProc; var myObj: TMyObject; begin myObj := nil; try myObj := TMyObject.Create; myObj.DoWhatever; finally myObj.free; end; end;
(If static analysis could prove that myObj can never be accessed before it gets assigned (as is the case here) it could even optimize out the initialization.)
There would be two other use cases for the [Local] attribute. Putting it inside a field list for a class or record would mean that when the class or record gets cleaned up, the object in question should be destroyed. (And I’ve got more to say about object and record cleanup a bit later on. There’s something that could be done to improve those greatly) And for functions, it would be a little bit different.
You’ve used factory functions before, right? You call a function that produces a new object, instead of using an existing one that you pass in. Well, [Local] would be useful for them, for safety:
function MyFactory(value, value2: integer): [Local] TMyObject; begin result := FactoryRegistryClass[integer].Create; result.Foo(value2); end;
The problem with a function like this is, what happens if the call to Foo can raise an exception? You don’t want a try/finally block here, because if everything goes right you’re supposed to return the value, but you do have to wrap it in a “try/except free; raise; end;” block to get it right. Marking the result as [Local] would have the compiler take care of that for you.
Tagging something that’s not an object or a typed pointer with [Local] should result in a compiler warning.
This scheme would result in much cleaner code, it would keep the responsibility for making the smart decisions where it belongs–with the programmer, not the language designer–it doesn’t introduce any overhead beyond that which exists for correct code, and it doesn’t break any already-existing code. Unlike ARC, where there’s really no upside, with this system there’s no downside.
…except for one thing. Moving object fields to an object’s or record’s automatic destruction list would mean moving them to FinalizeRecord, and for the life of me, I cannot understand why FinalizeRecord exists at all.
For those of you who’ve never ended up in there while debugging with Use Debug DCUs turned on, or who just plain don’t know what it is, FinalizeRecord (and its partner in crime, FinalizeArray) is a routine that uses RTTI to automatically clean up managed managed-type variables (strings, interfaces, variants) that are owned by something else. When you destroy an object that contains a string, you don’t have to clean up the string in the destructor, because FinalizeRecord takes care of it for you. The compiler creates a table that says where all of the managed references are and what types they are, and FinalizeRecord and FinalizeArray go over this table, and for each item in it they look up the correct cleanup routine and call it with a pointer to the right location in memory. It’s been around since the beginning of Delphi, and it’s been a big mistake since the beginning. It’s absolutely the wrong way to clean up this sort of situation.
I can sort of imagine how it came about. They’d just invented the Delphi RTTI system to take care of form streaming, which was quite the impressive accomplishment, and remains one today. (Remember that Java came out at the same time as Delphi, and still doesn’t have a decent form designer!) And someone on another part of the team said, “well, look at this. We’ve got managed types, that the compiler cleans up when they go out of scope without the programmer needing to write any code for it, and that works fine inside of procedures… but how are we going to handle it for objects that contain managed types as fields?”
And then someone, tragically, said “well, look at this cool RTTI thing we invented! We can write up one cleanup routine that inspects the object’s structure and cleans up managed types that way.” And so that’s what they did.
The only problem with that theory is that it requires the original Delphi team to have been smart enough to create Delphi, and yet stupid enough to make a mistake like this. It requires that Anders Hejlsberg, who created Delphi, who famously stated “static when possible, dynamic when necessary,” to have gotten his own principle completely wrong in this case. (But, since FinalizeRecord has been in Delphi since the beginning, any explanation of why it happened must necessarily involve this in some way. It really does baffle me.)
The thing is, RTTI is Run-Time type information. It’s for looking things up that are not and cannot be known at compile time, such as (de)serializing a form file. But this is not such a situation. Remember that the compiler has to build the finalization table in the first place. This means that it already knows exactly what has to be finalized, and seeing as how Delphi is not a dynamically-typed language, that knowledge it has isn’t going to become invalid at any point after compilation. So why doesn’t it simply do what compilers do best, and generate code to do the finalization?
Even though they’re written in hand-optimized assembly, FinalizeRecord and FinalizeArray are painfully slow. They’ll frequently turn up in profiling reports if you’re working with code that churns through a lot of objects or records. And adding objects to them will just make things slower. (Which the ARC system has done, BTW. It just keeps inflicting layer upon layer of extra slowness upon us!) What I’d like to see is those routines deleted from System.pas, and the VMT slot that points to the finalization table replaced with a pointer to an automatically-generated cleanup routine. No more unnecessary RTTI to take care of a task that could easily be covered under “static when possible”.
And before anyone objects, saying that debugging would suffer under all of the new auto-generated code… have you ever tried to debug into FinalizeArray? It’s a huge mess of ASM. What you would see in the debugger if you tried to trace into this would be a much smaller and more straightforward mess of ASM, so if anything, debugging would be improved!
If Embarcadero would do these two things–throw out ARC in favor of a [Local] attribute (don’t worry about compatibility if it’s only for one version, guys; Delphi developers are very forgiving and we’d let you take a mulligan on an obvious mistake; it wouldn’t be the first time we’ve done it) and throw out FinalizeArray and FinalizeRecord in favor of faster, compiler-generated static code, we’d see some huge improvements in both readability and performance for the Delphi language. That would give us a NextGen I could actually rant about positively!