Keyword mapping refactoring

This commit is contained in:
Michael Schimmel
2025-12-09 12:59:36 +01:00
parent 43afbd6050
commit f8dc5b945c
13 changed files with 257 additions and 188 deletions
+100 -77
View File
@@ -4,15 +4,17 @@ interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.Generics.Defaults,
System.SyncObjs;
type
// The runtime representation of an interned keyword
IKeyword = interface
{$region 'private'}
function GetIdx: Integer;
function GetName: string;
{$endregion}
property Idx: Integer read GetIdx;
property Name: string read GetName;
end;
@@ -33,10 +35,8 @@ type
strict private
class var
FLock: TSpinLock;
class var
FRegistry: TDictionary<string, Integer>;
class var
FReverseMap: TList<IKeyword>; // Use TList
FReverseMap: TList<IKeyword>;
class constructor Create;
class destructor Destroy;
public
@@ -49,12 +49,18 @@ type
// Defines a mapping from Keywords to a generic value T
IKeywordMapping<T> = interface
{$region 'private'}
function GetFields: TArray<TPair<IKeyword, T>>;
function GetItems(Idx: Integer): TPair<IKeyword, T>;
function GetFields(const Key: IKeyword): T;
function GetCount: Integer;
{$endregion}
// Finds the 0-based index for a given keyword. Returns -1 if not found.
function IndexOf(const Key: IKeyword): Integer;
// Gets all fields in the mapping.
property Fields: TArray<TPair<IKeyword, T>> read GetFields;
// Access value by Keyword (raises exception if not found)
property Fields[const Key: IKeyword]: T read GetFields;
// Access Key-Value pair by Index (0..Count-1)
property Items[Idx: Integer]: TPair<IKeyword, T> read GetItems; default;
property Count: Integer read GetCount;
end;
// Factory for creating IKeywordMapping instances
@@ -67,7 +73,11 @@ type
FMap: TArray<Integer>;
FFields: TArray<TPair<IKeyword, T>>;
FFirst, FLast: Integer;
function GetFields: TArray<TPair<IKeyword, T>>;
function GetItem(Idx: Integer): TPair<IKeyword, T>;
function GetItems(Idx: Integer): TPair<IKeyword, T>;
function GetFields(const Key: IKeyword): T;
function GetCount: Integer;
public
// Expects AFields to be in the desired (canonical) order
constructor Create(const AFields: TArray<TPair<IKeyword, T>>);
@@ -77,7 +87,6 @@ type
// Cache implementation
class var
FLock: TSpinLock;
class var
FRegistry: TDictionary<TArray<Integer>, IKeywordMapping<T>>;
class constructor Create;
class destructor Destroy;
@@ -89,10 +98,6 @@ type
implementation
uses
WinApi.Windows,
System.Generics.Defaults; // For TComparer<Integer>
{ TKeywordRegistry.TKeyword }
constructor TKeywordRegistry.TKeyword.Create(const AName: string; AIdx: Integer);
@@ -116,9 +121,9 @@ end;
class constructor TKeywordRegistry.Create;
begin
FLock := TSpinLock.Create(false);
FRegistry := TDictionary<string, Integer>.Create;
FReverseMap := TList<IKeyword>.Create;
FLock := TSpinLock.Create(False);
end;
class destructor TKeywordRegistry.Destroy;
@@ -127,18 +132,6 @@ begin
FReverseMap.Free;
end;
class function TKeywordRegistry.GetName(AIdx: Integer): string;
begin
FLock.Enter;
try
// Check bounds
if (AIdx >= 0) and (AIdx < FReverseMap.Count) then
Result := FReverseMap[AIdx].Name;
finally
FLock.Exit;
end;
end;
class function TKeywordRegistry.Intern(const AName: string): IKeyword;
var
idx: Integer;
@@ -147,72 +140,118 @@ begin
try
if not FRegistry.TryGetValue(AName, idx) then
begin
Result := TKeyword.Create(AName, FRegistry.Count);
FRegistry.Add(AName, FReverseMap.Add(Result));
idx := FReverseMap.Count;
Result := TKeyword.Create(AName, idx);
FReverseMap.Add(Result);
FRegistry.Add(AName, idx);
end
else
begin
Result := FReverseMap[idx];
end;
finally
FLock.Exit;
end;
end;
{ TKeywordMappingRegistry<T>.TKeywordMapping<T> }
class function TKeywordRegistry.GetName(AIdx: Integer): string;
begin
if (AIdx >= 0) and (AIdx < FReverseMap.Count) then
Result := FReverseMap[AIdx].Name
else
Result := '';
end;
{ TKeywordMappingRegistry<T>.TKeywordMapping }
constructor TKeywordMappingRegistry<T>.TKeywordMapping.Create(const AFields: TArray<TPair<IKeyword, T>>);
var
i, idx: Integer;
i: Integer;
keyIdx: Integer;
begin
inherited Create;
FFields := AFields;
FFirst := 0;
FLast := -1;
if Length(FFields) = 0 then
exit;
// Find min/max Idx for the map. Fields are *not* assumed to be sorted.
if Length(FFields) = 0 then
begin
FFirst := 0;
FLast := -1;
FMap := nil;
Exit;
end;
FFirst := FFields[0].Key.Idx;
FLast := FFirst;
for i := 1 to High(FFields) do
begin
idx := FFields[i].Key.Idx;
if FFirst > idx then
FFirst := idx;
if FLast < idx then
FLast := idx;
keyIdx := FFields[i].Key.Idx;
if keyIdx < FFirst then
FFirst := keyIdx;
if keyIdx > FLast then
FLast := keyIdx;
end;
SetLength(FMap, FLast - FFirst + 1);
for i := 0 to High(FMap) do
FMap[i] := -1;
// Populate map based on the *original field order*
for i := 0 to High(FFields) do
FMap[FFields[i].Key.Idx - FFirst] := i;
begin
keyIdx := FFields[i].Key.Idx;
FMap[keyIdx - FFirst] := i;
end;
end;
function TKeywordMappingRegistry<T>.TKeywordMapping.GetFields: TArray<TPair<IKeyword, T>>;
function TKeywordMappingRegistry<T>.TKeywordMapping.GetCount: Integer;
begin
Result := FFields;
Result := Length(FFields);
end;
// Internal helper required for interface implementation match if defined in stricter mode
function TKeywordMappingRegistry<T>.TKeywordMapping.GetItem(Idx: Integer): TPair<IKeyword, T>;
begin
Result := FFields[Idx];
end;
// Interface method for Items property
function TKeywordMappingRegistry<T>.TKeywordMapping.GetItems(Idx: Integer): TPair<IKeyword, T>;
begin
Result := FFields[Idx];
end;
// Interface method for Fields property
function TKeywordMappingRegistry<T>.TKeywordMapping.GetFields(const Key: IKeyword): T;
var
idx: Integer;
begin
idx := IndexOf(Key);
if idx < 0 then
raise EArgumentException.CreateFmt('Keyword "%s" not found in mapping.', [Key.Name]);
Result := FFields[idx].Value;
end;
function TKeywordMappingRegistry<T>.TKeywordMapping.IndexOf(const Key: IKeyword): Integer;
var
idx: Integer;
keyIdx, mapIdx: Integer;
begin
idx := Key.Idx - FFirst;
if (idx < 0) or (Idx > High(FMap)) then
exit(-1);
keyIdx := Key.Idx;
if (keyIdx < FFirst) or (keyIdx > FLast) then
Exit(-1);
Result := FMap[idx];
mapIdx := keyIdx - FFirst;
if (mapIdx < Length(FMap)) then
Result := FMap[mapIdx]
else
Result := -1;
end;
{ TKeywordMappingRegistry<T> }
class constructor TKeywordMappingRegistry<T>.Create;
begin
FLock := TSpinLock.Create(false);
FRegistry := TDictionary<TArray<Integer>, IKeywordMapping<T>>.Create;
FRegistry := TDictionary<TArray<Integer>, IKeywordMapping<T>>.Create(TEqualityComparer<TArray<Integer>>.Default);
FLock := TSpinLock.Create(False);
end;
class destructor TKeywordMappingRegistry<T>.Destroy;
@@ -222,36 +261,20 @@ end;
class function TKeywordMappingRegistry<T>.Intern(const AFields: TArray<TPair<IKeyword, T>>): IKeywordMapping<T>;
var
key: TArray<Integer>;
newMapping: IKeywordMapping<T>;
signature: TArray<Integer>;
i: Integer;
begin
SetLength(key, Length(AFields));
for var i := 0 to High(key) do
key[i] := AFields[i].Key.Idx;
SetLength(signature, Length(AFields));
for i := 0 to High(AFields) do
signature[i] := AFields[i].Key.Idx;
// 2. Lock
FLock.Enter;
try
// 3. Check cache
if FRegistry.TryGetValue(key, Result) then
exit; // Found it, exit
finally
FLock.Exit;
end;
// 4. Not in cache. Create (O(N+M)) - OUTSIDE lock
newMapping := TKeywordMapping.Create(AFields) as IKeywordMapping<T>;
// 5. Lock again to add
FLock.Enter;
try
// 6. Check again (double-check)
if FRegistry.TryGetValue(key, Result) then
exit; // Another thread won the race
// 7. Add our new mapping.
Result := newMapping;
FRegistry.Add(key, Result);
if not FRegistry.TryGetValue(signature, Result) then
begin
Result := TKeywordMapping.Create(AFields);
FRegistry.Add(signature, Result);
end;
finally
FLock.Exit;
end;
+30 -5
View File
@@ -3,6 +3,7 @@ unit Myc.Data.Records;
interface
uses
System.SysUtils,
System.Generics.Collections,
Myc.Data.Keyword,
Myc.Data.Value;
@@ -12,7 +13,11 @@ type
TDynamicRecord = class(TInterfacedObject, IKeywordMapping<TDataValue>)
private
FFields: TArray<TPair<IKeyword, TDataValue>>;
function GetFields: TArray<TPair<IKeyword, TDataValue>>;
// IKeywordMapping implementation
function GetItems(Idx: Integer): TPair<IKeyword, TDataValue>;
function GetFields(const Key: IKeyword): TDataValue;
function GetCount: Integer;
public
constructor Create(const AFields: TArray<TPair<IKeyword, TDataValue>>);
function IndexOf(const Key: IKeyword): Integer;
@@ -28,19 +33,39 @@ begin
FFields := AFields;
end;
function TDynamicRecord.GetFields: TArray<TPair<IKeyword, TDataValue>>;
function TDynamicRecord.GetCount: Integer;
begin
Result := FFields;
Result := Length(FFields);
end;
function TDynamicRecord.GetFields(const Key: IKeyword): TDataValue;
var
idx: Integer;
begin
idx := IndexOf(Key);
if idx < 0 then
raise EArgumentException.CreateFmt('Field "%s" not found in dynamic record.', [Key.Name]);
Result := FFields[idx].Value;
end;
function TDynamicRecord.GetItems(Idx: Integer): TPair<IKeyword, TDataValue>;
begin
Result := FFields[Idx];
end;
function TDynamicRecord.IndexOf(const Key: IKeyword): Integer;
var
i: Integer;
begin
// Linear search (O(n)). Fast enough for typical records.
// Keyword comparison is integer-based (Idx), so this is efficient.
for Result := 0 to High(FFields) do
for i := 0 to High(FFields) do
begin
if FFields[Result].Key.Idx = Key.Idx then
if FFields[i].Key.Idx = Key.Idx then
begin
Result := i;
exit;
end;
end;
Result := -1;
end;
+10 -9
View File
@@ -143,6 +143,7 @@ type
// A field definition for a scalar record.
TScalarRecordField = TPair<IKeyword, TScalar.TKind>;
IScalarRecordDefinition = IKeywordMapping<TScalar.TKind>;
IScalarRecord = IKeywordMapping<TScalar>;
TScalarRecordRegistry = TKeywordMappingRegistry<TScalar.TKind>;
// A record of scalar values, based on a definition.
@@ -804,7 +805,7 @@ end;
constructor TScalarRecord.Create(const ADef: IScalarRecordDefinition; const AFields: TArray<TScalar.TValue>);
begin
Assert(Length(ADef.Fields) = Length(AFields), 'Field definition and value count must match.');
Assert(ADef.Count = Length(AFields), 'Field definition and value count must match.');
FDef := ADef;
FFields := AFields;
end;
@@ -827,17 +828,17 @@ begin
if idx < 0 then
raise EArgumentException.CreateFmt('Field ":%s" not found in record definition.', [Key.Name]);
Result.Create(FDef.Fields[idx].Value, FFields[idx]);
Result.Create(FDef[idx].Value, FFields[idx]);
end;
{ TScalarRecordSeries }
constructor TScalarRecordSeries.Create(const ADef: IScalarRecordDefinition);
begin
Assert(Length(ADef.Fields) > 0);
Assert(ADef.Count > 0);
FDef := ADef;
FTotalCount := 0;
SetLength(FFields, Length(FDef.Fields));
SetLength(FFields, FDef.Count);
for var i := 0 to High(FFields) do
FFields[i] := TMemberSeries.Create(Self, i);
end;
@@ -851,7 +852,7 @@ end;
procedure TScalarRecordSeries.Add(const Item: TScalarRecord; Lookback: Int64 = -1);
begin
FArray.Add(Item.Fields, Length(FDef.Fields) * Lookback);
FArray.Add(Item.Fields, FDef.Count * Integer(Lookback));
inc(FTotalCount);
end;
@@ -862,7 +863,7 @@ type
var
len: Integer;
begin
len := Length(FDef.Fields);
len := FDef.Count;
// Use Slice to pass the raw data as an open array without copying.
FArray.Add(Slice(PValueArr(@Data)^, len), len * Integer(Lookback));
inc(FTotalCount);
@@ -879,7 +880,7 @@ end;
function TScalarRecordSeries.GetCount: Int64;
begin
Result := FArray.Count div Length(FDef.Fields);
Result := FArray.Count div FDef.Count;
end;
function TScalarRecordSeries.GetDef: IScalarRecordDefinition;
@@ -889,7 +890,7 @@ end;
function TScalarRecordSeries.GetItemRef(Idx: Integer): TChunkArray<TScalar.TValue>.PT;
begin
var len := Length(FDef.Fields);
var len := FDef.Count;
Result := FArray.ItemRef[(FArray.Count - len) - (Idx * len)];
end;
@@ -958,7 +959,7 @@ end;
constructor TScalarRecordSeries.TMemberSeries.Create(ARecordSeries: TScalarRecordSeries; AElementIdx: Integer);
begin
inherited Create(ARecordSeries);
FKind := ARecordSeries.FDef.Fields[AElementIdx].Value;
FKind := ARecordSeries.FDef[AElementIdx].Value;
FOffset := sizeof(TScalar.TValue) * AElementIdx;
end;
+7 -3
View File
@@ -300,6 +300,7 @@ var
field: TScalarRecordField;
genField: TPair<IKeyword, TDataValue>;
first: Boolean;
i: Integer;
begin
case FKind of
vkScalar: Result := FScalar.ToString;
@@ -317,10 +318,11 @@ begin
try
sb.Append('{');
first := True;
for field in series.Def.Fields do
for i := 0 to series.Def.Count - 1 do
begin
if not first then
sb.Append(',');
field := series.Def[i];
sb.Append(field.Key.Name);
first := False;
end;
@@ -338,10 +340,11 @@ begin
try
sb.Append('{');
first := True;
for field in rec.Def.Fields do
for i := 0 to rec.Def.Count - 1 do
begin
if not first then
sb.Append(',');
field := rec.Def[i];
sb.Append(field.Key.Name);
first := False;
end;
@@ -360,10 +363,11 @@ begin
try
sb.Append('{');
first := True;
for genField in rec.Fields do
for i := 0 to rec.Count - 1 do
begin
if not first then
sb.Append(',');
genField := rec[i];
sb.Append(genField.Key.Name);
first := False;
end;