Mapping Delphi Types to Indices at Compile Time
[SHOWTOGROUPS=4,20]
April 21, 2020 by Erik van Bilsen
Say what now? In this post I will show you how you can use generics and class variables to generate unique incrementing indices for any Delphi type. For example, the Integer type could map to index 1 and the TStream type to index 2 etc. The values of these indices will be determined (partially) at compile time.
Use Cases
But why would you need something like this? Granted, the number of use cases may be limited, but there are some useful ones. You can use this technique any time you need to associate some information with a particular Delphi type in a fast and memory efficient way.
For example, a while ago I played around with creating an Entity Component System library for Delphi.
A “traditional” approach to do this would be for each entity to maintain a dictionary that maps a component type to a Boolean that indicates whether the entity supports the component or not. For example, this could be a TDictionary<PTypeInfo, Boolean>, where the key is the type information for a component.
However, if you have lots of entities, each with its own dictionary, then this approach can be slow and use quite a bit of memory. Even though dictionaries are generally fast because they are based on hash tables, querying dictionaries thousands of times per second (which is not uncommon for an ECS) can have a considerable impact.
But what if each component type you care about has a simple index, starting at 0 and incrementing for each component type in use. Then the dictionary could be replaced with a simple array, as in array [0..MAX_TYPES-1] of Boolean. Or better yet, if there only a limited number of component types, say 64 or less, then the dictionary can be replaced with a single UInt64, where each bit represents a specific component type. This not only takes far less memory, but checking whether a component type is supported is as easy as checking if a specific bit is set, which is very fast.
A Simple Implementation
Generating type indices is actually pretty simple and only requires a few lines of code. The trick lies in using a combination of a generic type and static class variable:
You can find this code (and other code used in this post) in our JustAddCode repository on GitHub.
This code may look a bit unusual, but I will explain it in a bit. You can use the code like this:
Which will generate the following output:
As can be expected, type aliases (like the TIntegerAlias type in the example below) return the same index as their aliased type. If you need a unique index for another Integer type, then you can declare it as a distinct type instead of an alias (see TDistinctInteger below).
Which results in:
How Does It Work?
The key to this technique is the static class variable FValue. Because TTypeIndex<T> is a generic class, there is a different instance of FValue for each instantiated type of TTypeIndex<T>.
So it is not technically true that the type indices are assigned at compile time; they are assigned at runtime at application startup. But the determination which types get index values is made at compile time since the compiler will insert the class constructor code for each instantiated type.
[/SHOWTOGROUPS]
April 21, 2020 by Erik van Bilsen
Say what now? In this post I will show you how you can use generics and class variables to generate unique incrementing indices for any Delphi type. For example, the Integer type could map to index 1 and the TStream type to index 2 etc. The values of these indices will be determined (partially) at compile time.
Use Cases
But why would you need something like this? Granted, the number of use cases may be limited, but there are some useful ones. You can use this technique any time you need to associate some information with a particular Delphi type in a fast and memory efficient way.
For example, a while ago I played around with creating an Entity Component System library for Delphi.
The implementation requires that I keep track of which components are associated with which entities. A component is just a particular Delphi type in this case. There can be many, many entities and each entity can have any combination of components associated with it. The library needs to be able to quickly check if an entity supports a particular component.In case you are unfamiliar with Entity Components Systems, here is a highly simplified (and not very accurate) description: An ECS is a programming paradigm where Entities are kinda like classes, Components are kinda like properties and Systems are like a bunch of methods. But without classes, properties and methods, and optimized for performance.
Confused or intrigued? Let me know if you want to know more.
A “traditional” approach to do this would be for each entity to maintain a dictionary that maps a component type to a Boolean that indicates whether the entity supports the component or not. For example, this could be a TDictionary<PTypeInfo, Boolean>, where the key is the type information for a component.
However, if you have lots of entities, each with its own dictionary, then this approach can be slow and use quite a bit of memory. Even though dictionaries are generally fast because they are based on hash tables, querying dictionaries thousands of times per second (which is not uncommon for an ECS) can have a considerable impact.
But what if each component type you care about has a simple index, starting at 0 and incrementing for each component type in use. Then the dictionary could be replaced with a simple array, as in array [0..MAX_TYPES-1] of Boolean. Or better yet, if there only a limited number of component types, say 64 or less, then the dictionary can be replaced with a single UInt64, where each bit represents a specific component type. This not only takes far less memory, but checking whether a component type is supported is as easy as checking if a specific bit is set, which is very fast.
A Simple Implementation
Generating type indices is actually pretty simple and only requires a few lines of code. The trick lies in using a combination of a generic type and static class variable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | type TTypeIndex<T> = class // static private class var FValue: Integer; public class constructor Create; class property Value: Integer read FValue; end; var GNextTypeIndex: Integer = 0; { TTypeIndex<T> } class constructor TTypeIndex<T>.Create; begin FValue := GNextTypeIndex; Inc(GNextTypeIndex); end; |
You can find this code (and other code used in this post) in our JustAddCode repository on GitHub.
This code may look a bit unusual, but I will explain it in a bit. You can use the code like this:
1 2 3 4 | WriteLn('Index for type Integer: ', TTypeIndex<Integer>.Value); WriteLn('Index for type String: ', TTypeIndex<String>.Value); WriteLn('Index for type TStream: ', TTypeIndex<TStream>.Value); WriteLn('Index for type String: ', TTypeIndex<String>.Value); |
1 2 3 4 | Index for type Integer: 0 Index for type String: 1 Index for type TStream: 2 Index for type String: 1 |
Each type will have its own unique integer index, starting at 0. Every time you request the index for the same type, you get the same value (as you should, as you can see for the String type in the example). Requesting the type index is also extremely fast: it is just a single access to a static class variable, which is as efficient as loading a global variable.Note that you never create an instance of the TTypeIndex<T> class. It is a static class and you only need its class property Value.
As can be expected, type aliases (like the TIntegerAlias type in the example below) return the same index as their aliased type. If you need a unique index for another Integer type, then you can declare it as a distinct type instead of an alias (see TDistinctInteger below).
1 2 3 4 5 6 7 8 9 | type TIntegerAlias = Integer; TDistinctInteger = type Integer; WriteLn('Index for type TIntegerAlias: ', TTypeIndex<TIntegerAlias>.Value); WriteLn('Index for type TDistinctInteger: ', TTypeIndex<TDistinctInteger>.Value); |
1 2 | Index for type TIntegerAlias: 0 // Same index as Integer Index for type TDistinctInteger: 3 |
How Does It Work?
The key to this technique is the static class variable FValue. Because TTypeIndex<T> is a generic class, there is a different instance of FValue for each instantiated type of TTypeIndex<T>.
The compiler keeps track of which instantiated types are used in the code. In this example, there are 4 instantiated types: TTypeIndex<Integer>, TTypeIndex<String>, TTypeIndex<TStream> and TTypeIndex<TDistinctInteger>. Thus this means that there are 4 instances of the FValue class variable. And likewise, this means that the class constructor will get called 4 times at application startup, once for each instantiated type. It is there that we assign the FValue class variable for the type and increment a global index for the next type.As far as I remember, that has not always been the case. As I recall (but please correct me if I am wrong), in the first Delphi versions with support for generics, static class variables would be shared across all instantiated types. That is, there would be only one instance of FValue in this case, that would be shared with each specific version of TTypeIndex<T>. If that was still the case, then the technique discussed in this article would not work.
So it is not technically true that the type indices are assigned at compile time; they are assigned at runtime at application startup. But the determination which types get index values is made at compile time since the compiler will insert the class constructor code for each instantiated type.
[/SHOWTOGROUPS]