Saturday, March 29, 2014

Why Delphi generics are annoying

First of all: I like generics, I love them. You can do all kinds of neat stuff with them... until they bite you. Then they crash your compiler which might make your IDE hang or actually generate wrong source code.

So today I will talk about about the smaller or bigger annoyances of Delphi generics.

They turn your binary into a fat bastard


This might not be a problem at first but you might end up in a situation where your application reaches 36MB just because you are heavily using generic types. And actually duplicated code that is exactly the same for every bit. This by the way is one of the reasons your empty default applications are growing constantly for the recent releases of Delphi: because now generic collections are used all over the place in the runtime. And when I use TList<TButton>, boom, another 10K added to your binary size (approx in XE5). Too bad if you are using more advanced and feature rich collection types like Spring4D. Then every use adds 65K (actually we got down to that number from close to 100K a few months ago).

How could this be solved? Either by the linker fixing this by figuring out identical methods and removing duplicates (the C++ linker has an option for that called COMDAT folding) or the compiler itself could be smart and generate code for equal types only once. Of course he should pay attention to any use of TypeInfo(T) or T.Create for example because that code is actually different. C# does something like this.

Even worse when you think you just use an abstract class (like TEnumerable<T>) in an interface and it suddenly compiles in TList<T> although you never ever created one! Why? Because in the implementation of TEnumerable<T>.ToArray a TList<T> gets created. And because that method is public and virtual (I guess) it won't be smartlinked out.

If you ever tried delegating a generic interface with the implements keyword you either suffered from the bug I mentioned earlier or you realized that this again adds to the binary size because the compiler creates stub methods that makes this possible. I tried to achieve this using interface delegation but that did not result in reducing the binary size to a number I like (actually it was 15K just for interface stubs! Yes the IList<T> interface has quite some methods)

type
  TObjectList<T: class> = class(TList<TObject>, IList<T>)
    // ...
  end;

Interface methods must not have parameterized methods


Also known as compiler error E2535. So you cannot write something neat like this:

type
  IEnumerable<T> = interface
    function Select<TResult>(
      const selector: TFunc<T, TResult>): IEnumerable<TResult>;
  end;

C# has them and hell even Java has them! As far as I know this is because of some implementation detail and the same reason why you cannot have parameterized virtual methods.

No helpers for generic types


How much easier would that make our lives because we actually could extract some code from the generic class to a helper which we might not be used for every instantiation we are using and thus help us with the binary size problem. If we also had helpers for interfaces we could realize something like the method above in a helper because it does not need to be implemented in some class by design (is just needs the iterator). Someone might scream that helpers are not meant to be used by design but I disagree. I actually would like to have them improved so we could use more than one at a time. Even more so now that we have all these helpers for intrinsic types sitting in the SysUtils unit that make it impossible to add own behavior without hiding (or copying, argh) the built-in one. It could also avoid making ugly design decisions to make the underlying array of a TList<T> public to everyone without caring about the fact that it might contain garbage.

The compiler just goes bonkers


If you ever really heavily used generics you might experienced all kinds of funky internal errors where it suddenly stops and the marker points to the line after the last one in a unit or the compile times just goes beyond minutes you know what I am talking about. Even more if you tried to find workarounds to the previously mentioned problems. Like you made a generic record with lots of methods and watch the compiler spinning in circles performing "shlemiel" lookups for the stuff it is compiling. Or it completely fails doing anything and instead running in circles allocating memory until everything is lost.

Addendum:
After I wrote this I noticed another problem. Imagine this class:

type
  TFoo<T: class> = class
  strict private
    procedure Bar(const item: TObject); overload;
  public
    procedure Bar(const item: T); overload;
  end;

What method do you expect to be called when having a TFoo<TButton> and calling Bar passing in a TButton? The second of course. First it matches the argument and second it's the only public method. Do you think anything should be different when you have a TFoo<TObject>? No? Well the compiler does not agree and happily calls the private one.

No support for conditional compiling


This could solve the problem we discussed earlier where I have a generic but want to use specific implementations depending on the type parameter. Currently something like this is not possible:

function TCollections.CreateList: IList<T>;
begin
{$IF TypeInfo(T).Kind = tkInterface}
  IList<IInterface>(Result) := TList<IInterface>.Create;
{$ELSEIF TypeInfo(T).Kind = tkClass}
  IList<TObject>(Result) := TObjectList<TObject>.Create;
{$ELSE}
  Result := TList<T>.Create;
{$IFEND}
end;

Finding solutions


The good news for those concerned about binary size (and with Spring4D that supports the mobile platforms being released soon you should or your customers might hate you for apps that are dozens of MB big): Spring4D will support something similar to how C# handles generics. That means with the SUPPORT_GENERIC_FOLDING switch on (it is disabled by default and not supported on Delphi 2010) it will just create TList<TObject> and TList<IInterface> when you are using the TCollections.CreateObjectList<T> and the new TCollections.CreateInterfaceList<T> methods. That means only 2.5k overhead for every list you are creating that holds objects or interfaces (implementing something for pointers is left as an exercise to the reader).

Update 23.04.2015


With the Spring4D 1.2 version the previous described way is not necessary and supported anymore as using TCollections.CreateObjectList<T> or TCollections.CreateInterfaceList<T> automatically creates a very thin (less than 1K!) wrapper class to keep the type info for T but uses a TObjectList<TObject> or TList<IInterface> as its base since all the generated code would be 100% identical. If you are using XE7 and up this works even better because of the new intrinsic functions. With subsequent releases we will introduce this mechanism for more types but lists are usually the majority of the used collection types and thus caused most of the binary bloat.

No comments:

Post a Comment