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

Ɨ