TValue and other “Variants” like implementation tests
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.