The recent post from Mason Wheeler about TValue speed was very interesting. I always had the impression, that variants were slow, but never tested that. So this was a surprise to me. But how fast the variants really are? There are other “variants” like implementations out there, so I decided to test them in a common test. At least the ones I know that exist. The record implicit operator allows for some very flexible operations on different data types. So I tested 5 different approaches to “variant” implementation.

  • Variants (build in Delphi as long as I can remember :) )
  • TValue (new in Delphi 2010)
  • TAnyValue (recently added to my library)
  • TOmniValue (part of OmniThreadLibrary)
  • Bare-bone variant record implementation for comparison.

Here is the code I tested on. It is just a little changed version of Masons. It forces the compiler to include all lines (by calling Writeln on resulting values) and it averages over integers, strings and floating point values. Just integers seemed unfair to me. After all in real life we would use all possible combinations of data. The test also has range and overflow checks turned on.

The results:

Iterations: 10000000

Times are in milliseconds

Type Variants TValue TAnyValue TOmniValue TVariableRec
j := I 255 4070 916 128 175
j := I/5 332 4081 1400 3265 313
j := IntToStr(I) 4548 11051 7873 6429 2795
ALL 4905 18847 9932 9654 3255

And here is the test code:

program ValuesTest;
 
{$APPTYPE CONSOLE}
uses
  SysUtils,
  rtti,
  diagnostics,
  OtlCommon,
  Cromis.AnyValue;
 
const
  HUNDRED_MILLION = 10000000;
 
type
  TVariableRec = record
  private
    FFLoat: Extended;
    FString: string;
    FInteger: Integer;
    function GetAsFloat: Extended;
    procedure SetAsFloat(const Value: Extended);
    function GetAsInteger: Integer;
    procedure SetAsInteger(const Value: Integer);
    function GetAsString: string;
    procedure SetAsString(const Value: string);
  public
    class operator Implicit(const Value: string): TVariableRec;
    class operator Implicit(const Value: Integer): TVariableRec;
    class operator Implicit(const Value: Extended): TVariableRec;
    class operator Implicit(const Value: TVariableRec): string;
    class operator Implicit(const Value: TVariableRec): Integer;
    class operator Implicit(const Value: TVariableRec): Extended;
    property AsFloat: Extended read GetAsFloat write SetAsFloat;
    property AsString: string read GetAsString write SetAsString;
    property AsInteger: Integer read GetAsInteger write SetAsInteger;
  end;
 
procedure tryTValue;
var
  i: integer;
  j: TValue;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsExtended;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryAnyValue;
var
  i: integer;
  j: TAnyValue;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsFloat;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryOmniValue;
var
  i: integer;
  j: TOmniValue;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsExtended;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryVariants;
var
  i: integer;
  j: variant;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j;
    j := I / 5;
    value2 := j;
    j := IntToStr(I);
    value3 := j;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
procedure tryVarRec;
var
  i: integer;
  j: TVariableRec;
  value1: Integer;
  value2: Extended;
  value3: string;
begin
  for I := 1 to HUNDRED_MILLION do
  begin
    j := I;
    value1 := j.AsInteger;
    j := I / 5;
    value2 := j.AsFloat;
    j := IntToStr(I);
    value3 := j.AsString;
  end;
 
  Writeln(value1);
  Writeln(value2);
  Writeln(value3);
end;
 
var
  stopwatch: TStopWatch;
 
{ TVariableRec }
 
class operator TVariableRec.Implicit(const Value: Extended): TVariableRec;
begin
  Result.AsFloat := Value;
end;
 
function TVariableRec.GetAsFloat: Extended;
begin
  Result := FFLoat;
end;
 
function TVariableRec.GetAsInteger: Integer;
begin
  Result := FInteger;
end;
 
function TVariableRec.GetAsString: string;
begin
  Result := FString;
end;
 
class operator TVariableRec.Implicit(const Value: Integer): TVariableRec;
begin
  Result.AsInteger := Value;
end;
 
class operator TVariableRec.Implicit(const Value: TVariableRec): Integer;
begin
  Result := Value.AsInteger;
end;
 
class operator TVariableRec.Implicit(const Value: TVariableRec): Extended;
begin
  Result := Value.AsFloat;
end;
 
class operator TVariableRec.Implicit(const Value: string): TVariableRec;
begin
  Result.AsString := Value;
end;
 
class operator TVariableRec.Implicit(const Value: TVariableRec): string;
begin
  Result := Value.AsString;
end;
 
procedure TVariableRec.SetAsFloat(const Value: Extended);
begin
  FFLoat := Value;
end;
 
procedure TVariableRec.SetAsInteger(const Value: Integer);
begin
  FInteger := Value;
end;
 
procedure TVariableRec.SetAsString(const Value: string);
begin
  FString := Value;
end;
 
begin
  try
    stopwatch := TStopWatch.StartNew;
    tryVariants;
    stopwatch.Stop;
    writeln('Variants: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryTValue;
    stopwatch.Stop;
    writeln('TValue: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryAnyValue;
    stopwatch.Stop;
    writeln('TAnyValue: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryOmniValue;
    stopwatch.Stop;
    writeln('TOmniValue: ', stopwatch.ElapsedMilliseconds);
    stopwatch := TStopWatch.StartNew;
    tryVarRec;
    stopwatch.Stop;
    writeln('TVariableRec: ', stopwatch.ElapsedMilliseconds);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
 
  readln;
end.

It is obvious and expected that bare-bone record is the fastest. But Variants are not far behind and in real world the difference is negligible. My TAnyValue and TOmniValue from Primož are on the same level. But in most cases his implementation is faster because I use an interface to store the data in a class (because of my other code that uses interface based IAnyValue) and he stores most of the data as numeric type (only some as interface based class). I could speed up my implementation but I find it fast enough in most cases. And I will take elegance over speed if the difference is small enough. But TValue is slow, to slow probably to be used a lot. They should work on it in the future. It is a shame to throw it away since it brings quite a lot of power with it. I wonder if it is necessary for internal data to be stored as generics array

For those of you who don’t like to use variants or want so more flexibility you can look for TAnyValue (which was inspired upon looking at TOmniValue implementation) or TOmniValue. They both work under older version of Delphi as far as I know (Delphi 2005 and up).

If I missed something or if you know of another “variant” like implementation, please let me know. The purpose of this post is to explore this area as well as possible.