Recently I got an inquiry about the speed of my TAnyValue implementation. For the record TAnyValue is a TValue like implementation using records and implicit operators. It lacks in features compared to TValue but it is faster and it works on older Delphi versions (Delphi 2005 and up). I also like to tinker with things like this, so it was fun to play around with it. I was inspired by the TOmniValue which is part of OmniThreadLibrary. My goal was to eventually make it faster, to make it the fastest “variant” type implementation out there.

In the 2010 I did the initial tests with my initial version of the TAnyValue. You can find the results here. Back then TAnyValue was somehow on par with TOmniValue. It was quite slower then Variants and a simple variable record (which is the fastest it can be and is a good comparison on how fast you really are). TValue was catastrophic back then being by far the slowest solution because of generics which it used internally. I then improved my implementation over time being silent about it. But I came a long way and today my solution is the fastest out there. But It must be said that all solutions today are fast, the differences only matters if you use really a lot of these values (assignments) per second and you absolutely need the speed.

When doing such an implementation, the tricky part is to get a good balance between memory consumption and speed. The simplest and most crude solution would be to store every possiible data type you want to handle in the record as internal variable (field). This way you don’t have to explicitly assign memory or copy bytes. The compiler does it all and this is the fastest possible way. But it is also terrible in regards to memory consumption. I will make the case on my TAnyValue and the types it supports at this moment. If I had the simplest and fastest solution I would cover these types (I assume 32 bit compiler here):

  • Int64 (64)
  • Integer (32)
  • Extended (80)
  • WideString (variable)
  • AnsiString (variable)
  • String (variable)
  • Boolean (8)
  • TObject (32)
  • Pointer (32)
  • Cardinal (32)
  • TDateTime (80)
  • IInterface (32)

Ok this is a really dumb aproach but I want an upper memory consumption limit. Application with 10000000 such records, holding one integer each, consumed a whooping 706.244 KB of memory. A lot! On the other side of the spectrum you have two different solution. You can use variants for inner data storage but I really wanted to avoid them because what would be the point using them 🙂 Another very sleek approach is what TOmniValue has done. It uses one Int64 field for most of the data types with very smart assignments and one IInterface field for Extended and strings types. Basically for all types that need finalization as you cannot have a destructor in a record. The approach TOmniValue uses is good but if you work with strings and floating points a lot, it will be slow as interfaces are notoriously slow. I wanted something fast and still not to hard on memory consumption. So I came to this:

  TSimpleData = record
    case Byte of
      atInteger:   (VInteger: Integer);
      atCardinal:  (VCardinal: Cardinal);
      atBoolean:   (VBoolean: Boolean);
      atObject:    (VObject: TObject);
      atPointer:   (VPointer: Pointer);
      atClass:     (VClass: TClass);
      atWideChar:  (VWideChar: WideChar);
      atChar:      (VChar: AnsiChar);
    {$IFDEF AnyValue_UseLargeNumbers}
      atInt64:     (VInt64: Int64);
      atExtended:  (VExtended: Extended);
    {$ENDIF}
  end;
 
  TAnyValue = packed record
  private
    FValueType: TValueType;
  {$IFNDEF AnyValue_NoInterfaces}
    FInterface: IInterface;
  {$ENDIF}
    FSimpleData: TSimpleData;
    FComplexData: array of Byte;
    ...
  end;

I use three fields. I use IInterface only for interfaces, thus the conditional define, so you can easily turn them off if you don’t need them and so save memory. Also the trick here is in the variable record. The good side of this record is, it only takes the amount of memory that the largest field does. In my case this is 32 bit, unless “AnyValue_UseLargeNumbers” is defined, then this is 80 bit. This way I cut down on size dramatically, by 48 bit per record. Finally ,there is a dynamic array of bytes, for strings and floating point values if “AnyValue_UseLargeNumbers” is not defined. So lets look that the memory consumption compared to others (again holding 32 bit integers and 10000000 records):

TAnyValue (no defines): 128.960 KB
TAnyValue (AnyValue_NoInterfaces): 89.812 KB
TAnyValue (AnyValue_UseLargeNumbers): 246.376 KB
TAnyValue (AnyValue_NoInterfaces and AnyValue_UseLargeNumbers): 207.228 KB
TOmniValue: 128.960 KB
TValue: 246.376 KB (wow not only is TValue slow but takes a lot of memory)
Variants: 158.308 KB

The only problem when not using “AnyValue_UseLargeNumbers” is that if you then actually use floating point types, you will consume more memory then without that directive. And you will be a little bit slower. You can still use floating points but they should not be in majority. So you can tweak TAnyValue to step up to the task at hand. I should also add that numbers would naturally be different if other data type would be used. But for overall picture 32 bit Integer is just fine.

Now lets look at speed results. The test application is the same as it was last time.

Type Variants TValue TAnyValue TOmniValue TVariableRec
j := I 178 3431 62 108 68
j := I/5 187 3968 230 4054 199
j := IntToStr(I) 5491 10593 4342 6632 2728
ALL 4479 19541 4158 10966 3140

As you can see TAnyValue gained a lot of speed, everything else is the same as it was back then. I also did the full test on XE3 to see if TValue improved in any way. The results are bellow

Type Variants TValue TAnyValue TOmniValue TVariableRec
ALL 3846 6497 3318 10613 2062

It is clear that they worked on TValue which is now fast enough for use. In fact it is very fast. Given together with flexibility it is a powerful tool.

Probably a lot of you wonder why even bother. I bother because I can. I like to tweak the code and see if I can make it even better. So if any of you have ideas how to make it even smaller in regards to memory consumption and retain the speed throne, please let me know 🙂

 

P.S.
If you are looking for TAnyValue you can find it as a part of Cromis Library in the downloads section.