Jump to content
Primož Gabrijelčič

Caching with class variables

Recommended Posts

Original post: https://www.thedelphigeek.com/2019/01/caching-with-class-variables.html

Recently I was extending a swiss-army-knife helper record we are using at work and I noticed a small piece of suboptimal code. At first I let it be as I couldn’t think of a simple way of improving the code – and the problem was really not so big that it should be fixed with a complicated solution. At some point, however, a simple and elegant solution appeared to me and I like it so much that I want to share it with the world 😉
 
Instead of showing our helper record in full, I have extracted just a small part of functionality, enough to do a simple demo. The Check method takes an integer, makes sure that it can be cast into an enumerated type or set T and returns the cast type:
type
  Range<T> = record
  private
    class function MaxIntVal: Integer; static; inline;
    class function MinIntVal: Integer; static; inline;
  public
    class function Check(const value: Integer): T; static;
  end;

class function Range<T>.Check(const value: Integer): T;
begin
  if (value < MinIntVal) or (value > MaxIntVal) then
    raise Exception.CreateFmt(
      'Value %d lies outside allowed range for %s (%d .. %d)',
      [value, PTypeInfo(TypeInfo(T)).Name, MinIntVal, MaxIntVal]);
  Move(value, Result, SizeOf(Result));
end;

 

Calling Range<TEnum>(i) works the same as executing TEnum(i) with an added bonus of checking for under- and overflows. The following code fragment shows how this function could be used:

type
  TEnum = (en1, en2, en3);
  TEnumSet = set of TEnum;

var
  en: TEnum;
  ens: TEnumSet;

en := Range<TEnum>.Check(2); // OK, en = en3
en := Range<TEnum>.Check(3); // exception

ens := Range<TEnumSet>.Check(0); // OK, ens = []
ens := Range<TEnumSet>.Check(8); // exception

 

The Check function uses following two helper functions to determine lowest and highest possible value for type T:

class function Range<T>.MaxIntVal: Integer;
var
  ti: PTypeInfo;
  typeData: PTypeData;
  isSet: Boolean;
  i: Integer;
begin
  ti := TypeInfo(T);
  isSet := ti.Kind = tkSet;
  if isSet then
    ti := GetTypeData(ti).CompType^;
  typeData := GetTypeData(ti);
  if isSet then
  begin
    Result := 0;
    for i := typeData.MinValue to typeData.MaxValue do
      Result := Result or (1 shl i);
  end
  else
    Result := typeData.MaxValue;
end;

class function Range<T>.MinIntVal: Integer;
var
  ti: PTypeInfo;
  typeData: PTypeData;
begin
  ti := TypeInfo(T);
  if ti.Kind = tkSet then
    ti := GetTypeData(ti).CompType^;
  typeData := GetTypeData(ti);
  Result:= typeData.MinValue;
end;

 

The suboptimal behaviour comes from the fact that MinIntVal and MaxIntVal are calculated each time Check is called. As type T doesn’t change while the program is being executed, it would suffice to call these two functions once and cache the result. The problem with this solution, however, is twofold. Firstly, this cache would have to exist somewhere. Some part of code would have to manage it. Secondly, it would have to be quite fast. MinIntVal and MaxIntVal, as implemented now, are not very slow and looking up that data in a cache could easily be slower than the current code.
As it turns out, we can fix both problems simply by using class variables, properties, and methods functionality of the Delphi language:

type
  TypeInfoCache<T> = class
  class var
    FMinIntVal: Integer;
    FMaxIntVal: Integer;
  public
    class constructor Create;
    class property MaxIntVal: Integer read FMaxIntVal;
    class property MinIntVal: Integer read FMinIntVal;
  end;

class constructor TypeInfoCache<T>.Create;
var
  ti: PTypeInfo;
  typeData: PTypeData;
  isSet: Boolean;
  i: Integer;
begin
  ti := TypeInfo(T);
  isSet := ti.Kind = tkSet;
  if isSet then
    ti := GetTypeData(ti).CompType^;
  typeData := GetTypeData(ti);
  FMinIntVal := typeData.MinValue;

  if isSet then
  begin
    FMaxIntVal := 0;
    for i := typeData.MinValue to typeData.MaxValue do
      FMaxIntVal := FMaxIntVal or (1 shl i);
  end
  else
    FMaxIntVal := typeData.MaxValue;
end;

 

A class constructor is called only once for each type T used in the code. It is also called automatically and we don’t have to take care of that. Moving the code that calculates min/max values for a type T into a class constructor therefore solves the first problem. To make sure that class part of the TypeInfoCache<T> was created, we merely have to access it, nothing more. The code in Range<T> can be replaced with simple one-liners:

class function Range<T>.MaxIntVal: Integer
begin
  Result := TypeInfoCache<T>.MaxIntVal;
end;

class function Range<T>.MinIntVal: Integer;
begin
  Result := TypeInfoCache<T>.MinIntVal;
end;

 

This also solves the second problem, as the access to a class variables doesn’t require any complications usually associated with a dictionary access. Accessing MinIntVal, for example, is a simple call into method that executes few mov instructions.

 

A demonstration project for this new improved solution is available here.

 

This approach is very limited in use – it can only be used to associate data with a type T – but neatly illustrates the power of the Delphi language.

Edited by Primož Gabrijelčič

Share this post


Link to post

Hi Primož,

 

that's a nice piece of code, but i notice in my Delphi Tokyo, that the ens-Result for the TEnumSet (for valid values) is empty.

But if you add a untyped Move, than it works:

 

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.TypInfo,
  System.Rtti,
  System.SysUtils;

resourcestring
  SValueLiesOutsideAllowedRange = 'Value %d lies outside allowed range for %s (%d .. %d)';

type
  TypeInfoCache<T> = class
  class var
    FMinIntVal: Integer;
    FMaxIntVal: Integer;
    FIsSet: Boolean;
  public
    class constructor Create;
    class property MaxIntVal: Integer read FMaxIntVal;
    class property MinIntVal: Integer read FMinIntVal;
    class property IsSet: Boolean read FIsSet;
  end;

  Range<T> = record
  private
    class function MaxIntVal: Integer; static; inline;
    class function MinIntVal: Integer; static; inline;
    class procedure RaiseException(const Value: Integer); static;
  public
    class function Check(const Value: Integer): T; static;
  end;

  { Range<T> }

class function Range<T>.Check(const Value: Integer): T;
begin
  if (Value < MinIntVal) or (Value > MaxIntVal) then
    RaiseException(Value);

  if TypeInfoCache<T>.IsSet then
  begin
    Move(Value, Result, SizeOf(T)); // here is the magic
  end;
end;

class function Range<T>.MaxIntVal: Integer;
begin
  Result := TypeInfoCache<T>.MaxIntVal;
end;

class function Range<T>.MinIntVal: Integer;
begin
  Result := TypeInfoCache<T>.MinIntVal;
end;

class procedure Range<T>.RaiseException(const Value: Integer);
begin
  raise Exception.CreateFmt(SValueLiesOutsideAllowedRange,
    [Value, PTypeInfo(TypeInfo(T)).Name, MinIntVal, MaxIntVal]);
end;

{ TypeInfoCache<T> }

class constructor TypeInfoCache<T>.Create;
var
  ti: PTypeInfo;
  typeData: PTypeData;
  i: Integer;
begin
  ti := TypeInfo(T);
  FIsSet := ti.Kind = tkSet;
  if FIsSet then
    ti := GetTypeData(ti).CompType^;
  typeData := GetTypeData(ti);
  FMinIntVal := typeData.MinValue;

  if FIsSet then
  begin
    FMaxIntVal := 0;
    for i := typeData.MinValue to typeData.MaxValue do
      FMaxIntVal := FMaxIntVal or (1 shl i);
  end
  else
    FMaxIntVal := typeData.MaxValue;
end;

type
  TEnum = (en1, en2, en3);
  TEnumSet = set of TEnum;

var
  en: TEnum;
  ens: TEnumSet;

begin
  try

    try
      en := Range<TEnum>.Check(0);
      en := Range<TEnum>.Check(2);
      en := Range<TEnum>.Check(3);
    except
      on E: Exception do
        Writeln('Expected exception: ', E.ClassName, ' ', E.Message);
    end;

    try
      ens := Range<TEnumSet>.Check(0);
      ens := Range<TEnumSet>.Check(2);
      ens := Range<TEnumSet>.Check(7);
      ens := Range<TEnumSet>.Check(8);
    except
      on E: Exception do
        Writeln('Expected exception: ', E.ClassName, ' ', E.Message);
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  if DebugHook <> 0 then
    Readln;

end.

 

  • Like 1

Share this post


Link to post

Exactly - I somehow deleted that line from the gist, while still keeping it in my local copy of the code. 😞 😞 😞

 

(Actually, this Move should not be 'if'-ed. Just always execute it.)

 

I'll fix my blog, gist example and this post when I come home later today.

 

Thank you for finding the bug!

  • Like 1

Share this post


Link to post

I've lost the thread after this line "As it turns out, we can fix both problems simply by using class variables, properties, and methods functionality of the Delphi language:"

The same code follows as already above the paragraph was., instead of the declaration of "TypeInfoCache" I think.

  • Like 1

Share this post


Link to post

The approach to store Min and Max Value for sets does not work as sets in Delphi can be max size of 32 Byte (set of Byte) - good luck storing that in an Integer.

 

The core idea though is nice - I think I have also done this before: use a generic type as storage for type related values.

Edited by Stefan Glienke

Share this post


Link to post
2 hours ago, Attila Kovacs said:

I've lost the thread after this line "As it turns out, we can fix both problems simply by using class variables, properties, and methods functionality of the Delphi language:"

The same code follows as already above the paragraph was., instead of the declaration of "TypeInfoCache" I think.

Copy&paste bug, sorry. Was OK on the blog, wrong here in the forum. Corrected.

 

Thank you!

Share this post


Link to post
39 minutes ago, Primož Gabrijelčič said:

We don't pass large sets through that function and everything is fine.

Been there, done that. And one day the enum gets its 33. element and stuff subtly starts failing 😉 

Share this post


Link to post

Don't think so as we don't use crazily big sets. Plus this code is mostly used for compatibility reason; because some old configuration files store enum and set data as integers.

 

But of course, all code that can fail, will fail.

Share this post


Link to post

 

19 minutes ago, Stefan Glienke said:

And one day the enum gets its 33

Actually at 32 will already fail, because he uses integer instead of cardinal.

But indeed, an "if typeData.MaxValue > 30 then raise.." would make this approach safer.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×