Browse Source

RbTree.ChangeInPlace for cheaper reinsertions.

Rika Ichinose 3 months ago
parent
commit
463a14e295
1 changed files with 131 additions and 19 deletions
  1. 131 19
      rtl/inc/heap.inc

+ 131 - 19
rtl/inc/heap.inc

@@ -198,10 +198,19 @@ type
     procedure Add(n: pNode);
     procedure Add(n: pNode);
     procedure Remove(n: pNode);
     procedure Remove(n: pNode);
     function LowerBound(key: pointer): pNode; { Returns smallest node ≥ key. }
     function LowerBound(key: pointer): pNode; { Returns smallest node ≥ key. }
-  {$ifdef DEBUG_HEAP_INC}
     function First: pNode;
     function First: pNode;
     class function Next(n: pNode): pNode; static;
     class function Next(n: pNode): pNode; static;
-  {$endif DEBUG_HEAP_INC}
+    class function Prev(n: pNode): pNode; static;
+
+    { ChangeInPlace is the same optimization used in Boost rbtree_best_fit.hpp:
+      shortcut Remove + Add if the node is already at correct position.
+
+      Callers check this case manually: if the node value is decreased, ChangeInPlace is possible if the new value is still ≥ Prev(node), and vice versa.
+
+      Beware of the overlaps... ChangeInPlace is potentially vulnerable to the RbTree.Node overlap in TryResizeVar,
+      so sizeof(RbTree.Node) <= VarSizeQuant is ensured with a compile-time $if. }
+
+    procedure ChangeInPlace(old, new: pNode);
 
 
   private const
   private const
     BlackTag = 1;
     BlackTag = 1;
@@ -270,7 +279,7 @@ type
     SizeIndexMask = 1 shl SizeIndexBits - 1;
     SizeIndexMask = 1 shl SizeIndexBits - 1;
     FixedBitPos = {$if SizeIndexBits >= 3} SizeIndexBits {$else} 3 {$endif}; { Variable chunks use 3 low bits for used / first / last. }
     FixedBitPos = {$if SizeIndexBits >= 3} SizeIndexBits {$else} 3 {$endif}; { Variable chunks use 3 low bits for used / first / last. }
     FixedFlag = 1 shl FixedBitPos;
     FixedFlag = 1 shl FixedBitPos;
-    ChunkOffsetShift = FixedBitPos + 1;
+    ChunkOffsetShift = {$if FixedBitPos + 1 >= 5} FixedBitPos + 1 {$else} 5 {$endif}; { VarSizeQuant must be at least 2^5 to fit 3 64-bit pointers (RbTree.Node). }
 
 
     { Not really used; MaxFixedChunkSize limit on fixed OS chunks assumed to be strictly enforced and (much!) more restricting than MaxChunkOffset.
     { Not really used; MaxFixedChunkSize limit on fixed OS chunks assumed to be strictly enforced and (much!) more restricting than MaxChunkOffset.
       MaxFixedChunkSize = 256 Kb.
       MaxFixedChunkSize = 256 Kb.
@@ -281,7 +290,7 @@ type
     UsedFlag = 1 shl 0;
     UsedFlag = 1 shl 0;
     FirstFlag = 1 shl 1;
     FirstFlag = 1 shl 1;
     LastFlag = 1 shl 2;
     LastFlag = 1 shl 2;
-    VarSizeQuant = 1 shl ChunkOffsetShift; {$if VarSizeQuant < Alignment} {$error Assumed to be >= Alignment.} {$endif}
+    VarSizeQuant = 1 shl ChunkOffsetShift; {$if VarSizeQuant < Alignment} {$error Assumed to be >= Alignment.} {$endif} {$if VarSizeQuant < 3 * sizeof(pointer)} {$error Must fit RbTree.Node.} {$endif}
     VarSizeMask = SizeUint(-VarSizeQuant);
     VarSizeMask = SizeUint(-VarSizeQuant);
     HugeHeader = 0; { Special header value for huge chunks. FixedFlag must be 0, and the value must be impossible for a variable chunk. 0 turns out to be suitable. :) }
     HugeHeader = 0; { Special header value for huge chunks. FixedFlag must be 0, and the value must be impossible for a variable chunk. 0 turns out to be suitable. :) }
 
 
@@ -561,7 +570,6 @@ type
       until false;
       until false;
   end;
   end;
 
 
-{$ifdef DEBUG_HEAP_INC}
   function RbTree.First: pNode;
   function RbTree.First: pNode;
   var
   var
     child: pNode;
     child: pNode;
@@ -579,10 +587,13 @@ type
   var
   var
     child: pNode;
     child: pNode;
   begin
   begin
-    result := n^.right;
-    if Assigned(result) then
+    child := n^.right;
+    if Assigned(child) then
     begin
     begin
-      while Assigned(result^.left) do result := result^.left;
+      repeat
+        result := child;
+        child := result^.left;
+      until not Assigned(child);
       exit;
       exit;
     end;
     end;
     result := n;
     result := n;
@@ -591,7 +602,26 @@ type
       result := pointer(result^.parentColor and ParentMask);
       result := pointer(result^.parentColor and ParentMask);
     until not Assigned(result) or (child = result^.left);
     until not Assigned(result) or (child = result^.left);
   end;
   end;
-{$endif DEBUG_HEAP_INC}
+
+  class function RbTree.Prev(n: pNode): pNode;
+  var
+    child: pNode;
+  begin
+    child := n^.left;
+    if Assigned(child) then
+    begin
+      repeat
+        result := child;
+        child := result^.right;
+      until not Assigned(child);
+      exit;
+    end;
+    result := n;
+    repeat
+      child := result;
+      result := pointer(result^.parentColor and ParentMask);
+    until not Assigned(result) or (child = result^.right);
+  end;
 
 
   procedure RbTree.ChangeChild(old, new, parent: pNode);
   procedure RbTree.ChangeChild(old, new, parent: pNode);
   begin
   begin
@@ -601,6 +631,22 @@ type
       root := new;
       root := new;
   end;
   end;
 
 
+  procedure RbTree.ChangeInPlace(old, new: pNode);
+  var
+    pc: PtrUint;
+    adj: pNode;
+  begin
+    adj := old^.left;
+    new^.left := adj;
+    if Assigned(adj) then adj^.parentColor := PtrUint(new) + adj^.parentColor and BlackTag;
+    adj := old^.right;
+    new^.right := adj;
+    if Assigned(adj) then adj^.parentColor := PtrUint(new) + adj^.parentColor and BlackTag;
+    pc := old^.parentColor;
+    new^.parentColor := pc;
+    ChangeChild(old, new, pointer(pc and ParentMask));
+  end;
+
   procedure RbTree.RotateSetParents(old, new: pNode; color: PtrUint);
   procedure RbTree.RotateSetParents(old, new: pNode; color: PtrUint);
   var
   var
     oldParentColor: PtrUint;
     oldParentColor: PtrUint;
@@ -1194,9 +1240,10 @@ type
 
 
   function HeapInc.ThreadState.AllocVar(size: SizeUint): pointer;
   function HeapInc.ThreadState.AllocVar(size: SizeUint): pointer;
   var
   var
-    fv: pFreeVarChunk;
+    fv, toRemove: pFreeVarChunk;
     osChunk, osNext: pVarOSChunk;
     osChunk, osNext: pVarOSChunk;
     vSize, minSize, maxSize, statv: SizeUint;
     vSize, minSize, maxSize, statv: SizeUint;
+    adj: RbTree.pNode;
   begin
   begin
     size := (size + (VarHeaderSize + VarSizeQuant - 1)) and SizeUint(-VarSizeQuant);
     size := (size + (VarHeaderSize + VarSizeQuant - 1)) and SizeUint(-VarSizeQuant);
   {$ifdef FPC_HAS_FEATURE_THREADING}
   {$ifdef FPC_HAS_FEATURE_THREADING}
@@ -1209,10 +1256,11 @@ type
 
 
     if Assigned(fv) then
     if Assigned(fv) then
     begin
     begin
-      varFree.Remove(pointer(fv));
       dec(pointer(fv), FreeVarChunkRbnOffset); { Now fv is fair pFreeVarChunk. }
       dec(pointer(fv), FreeVarChunkRbnOffset); { Now fv is fair pFreeVarChunk. }
+      toRemove := fv; { Postpone the removal to maybe ChangeInPlace. }
     end else
     end else
     begin
     begin
+      toRemove := nil;
       minSize := VarOSChunkDataOffset + size;
       minSize := VarOSChunkDataOffset + size;
       if minSize <= GrowHeapSize1 then
       if minSize <= GrowHeapSize1 then
         maxSize := GrowHeapSize1
         maxSize := GrowHeapSize1
@@ -1253,6 +1301,17 @@ type
       (vSize >= MinVarHeaderAndPayload) and (pVarHeader(result - VarHeaderSize)^.ch.h and LastFlag = 0) then
       (vSize >= MinVarHeaderAndPayload) and (pVarHeader(result - VarHeaderSize)^.ch.h and LastFlag = 0) then
     begin
     begin
       inc(pointer(fv), size); { result = allocated block, fv = remainder. }
       inc(pointer(fv), size); { result = allocated block, fv = remainder. }
+      if Assigned(toRemove) then
+      begin
+        adj := varFree.Prev(@toRemove^.rbn);
+        if not Assigned(adj) or (vSize >= pFreeVarChunk(pointer(adj) - FreeVarChunkRbnOffset)^.size) then { Tree position intact (free chunk size is always decreased). }
+          varFree.ChangeInPlace(@toRemove^.rbn, @fv^.rbn)
+        else
+        begin
+          varFree.Remove(@toRemove^.rbn);
+          toRemove := nil;
+        end;
+      end;
       pVarHeader(pointer(fv) - VarHeaderSize)^.prevSize := size;
       pVarHeader(pointer(fv) - VarHeaderSize)^.prevSize := size;
     {$ifdef FPC_HAS_FEATURE_THREADING}
     {$ifdef FPC_HAS_FEATURE_THREADING}
       pVarHeader(pointer(fv) - VarHeaderSize)^.threadState := @self;
       pVarHeader(pointer(fv) - VarHeaderSize)^.threadState := @self;
@@ -1263,13 +1322,16 @@ type
       if pVarHeader(result - VarHeaderSize)^.ch.h and LastFlag = 0 then
       if pVarHeader(result - VarHeaderSize)^.ch.h and LastFlag = 0 then
         pVarHeader(pointer(fv) + vSize - VarHeaderSize)^.prevSize := vSize;
         pVarHeader(pointer(fv) + vSize - VarHeaderSize)^.prevSize := vSize;
 
 
-      varFree.Add(@fv^.rbn);
+      if not Assigned(toRemove) then
+        varFree.Add(@fv^.rbn);
 
 
       { Allocated chunk is still first in the OS chunk if the original chunk was first. }
       { Allocated chunk is still first in the OS chunk if the original chunk was first. }
       pVarHeader(result - VarHeaderSize)^.ch.h := pVarHeader(result - VarHeaderSize)^.ch.h and FirstFlag + UsedFlag + uint32(size);
       pVarHeader(result - VarHeaderSize)^.ch.h := pVarHeader(result - VarHeaderSize)^.ch.h and FirstFlag + UsedFlag + uint32(size);
     end else
     end else
     begin
     begin
       { Use the entire chunk. }
       { Use the entire chunk. }
+      if Assigned(toRemove) then
+        varFree.Remove(@toRemove^.rbn);
       size := fv^.size;
       size := fv^.size;
       pVarHeader(result - VarHeaderSize)^.ch.h := pVarHeader(result - VarHeaderSize)^.ch.h and (FirstFlag or LastFlag) + UsedFlag + uint32(size);
       pVarHeader(result - VarHeaderSize)^.ch.h := pVarHeader(result - VarHeaderSize)^.ch.h and (FirstFlag or LastFlag) + UsedFlag + uint32(size);
     end;
     end;
@@ -1286,6 +1348,8 @@ type
     fSizeFlags: SizeUint;
     fSizeFlags: SizeUint;
     osChunk, osPrev, osNext: pVarOSChunk;
     osChunk, osPrev, osNext: pVarOSChunk;
     freeOsNext: pFreeOSChunk;
     freeOsNext: pFreeOSChunk;
+    toRemove: pFreeVarChunk;
+    adj: RbTree.pNode;
   begin
   begin
   {$ifdef FPC_HAS_FEATURE_THREADING}
   {$ifdef FPC_HAS_FEATURE_THREADING}
     if pVarHeader(p - VarHeaderSize)^.threadState <> @self then
     if pVarHeader(p - VarHeaderSize)^.threadState <> @self then
@@ -1308,6 +1372,7 @@ type
     fSizeFlags := pVarHeader(p - VarHeaderSize)^.ch.h;
     fSizeFlags := pVarHeader(p - VarHeaderSize)^.ch.h;
     result := fSizeFlags and VarSizeMask;
     result := fSizeFlags and VarSizeMask;
     dec(used, result);
     dec(used, result);
+    toRemove := nil;
 
 
     { If next/prev are free, remove them from varFree and merge with f — (f)uture (f)ree chunk that starts at p, has fSizeFlags,
     { If next/prev are free, remove them from varFree and merge with f — (f)uture (f)ree chunk that starts at p, has fSizeFlags,
       and conveniently always inherits prevSize of its final location. }
       and conveniently always inherits prevSize of its final location. }
@@ -1317,7 +1382,7 @@ type
       if pVarHeader(p2 - VarHeaderSize)^.ch.h and UsedFlag = 0 then
       if pVarHeader(p2 - VarHeaderSize)^.ch.h and UsedFlag = 0 then
       begin
       begin
         fSizeFlags := (fSizeFlags + pFreeVarChunk(p2)^.size) + pVarHeader(p2 - VarHeaderSize)^.ch.h and LastFlag;
         fSizeFlags := (fSizeFlags + pFreeVarChunk(p2)^.size) + pVarHeader(p2 - VarHeaderSize)^.ch.h and LastFlag;
-        varFree.Remove(@pFreeVarChunk(p2)^.rbn);
+        toRemove := p2;
       end;
       end;
     end;
     end;
 
 
@@ -1328,7 +1393,16 @@ type
       begin
       begin
         p := p2;
         p := p2;
         fSizeFlags := (fSizeFlags + pFreeVarChunk(p2)^.size) + pVarHeader(p2 - VarHeaderSize)^.ch.h and FirstFlag;
         fSizeFlags := (fSizeFlags + pFreeVarChunk(p2)^.size) + pVarHeader(p2 - VarHeaderSize)^.ch.h and FirstFlag;
-        varFree.Remove(@pFreeVarChunk(p2)^.rbn);
+        { Keep the largest chunk in toRemove as it will have more chances for ChangeInPlace succeeding. }
+        if Assigned(toRemove) then
+          if pFreeVarChunk(p2)^.size >= toRemove^.size then
+          begin
+            varFree.Remove(@toRemove^.rbn);
+            toRemove := p2;
+          end else
+            varFree.Remove(@pFreeVarChunk(p2)^.rbn)
+        else
+          toRemove := p2;
       end;
       end;
     end;
     end;
 
 
@@ -1336,15 +1410,32 @@ type
       unless it spans the entire OS chunk, in which case instead move the chunk from varOS to freeOS. }
       unless it spans the entire OS chunk, in which case instead move the chunk from varOS to freeOS. }
     if fSizeFlags and (FirstFlag or LastFlag) <> FirstFlag or LastFlag then
     if fSizeFlags and (FirstFlag or LastFlag) <> FirstFlag or LastFlag then
     begin
     begin
+      if Assigned(toRemove) then
+      begin
+        adj := varFree.Next(@toRemove^.rbn);
+        if not Assigned(adj) or (fSizeFlags and VarSizeMask <= pFreeVarChunk(pointer(adj) - FreeVarChunkRbnOffset)^.size) then { Tree position intact (free chunk size is always increased). }
+        begin
+          if toRemove <> p then { Might happen. Skip a no-op ChangeInPlace. }
+            varFree.ChangeInPlace(@toRemove^.rbn, @pFreeVarChunk(p)^.rbn);
+        end else
+        begin
+          varFree.Remove(@toRemove^.rbn);
+          toRemove := nil;
+        end;
+      end;
+
       if fSizeFlags and LastFlag = 0 then
       if fSizeFlags and LastFlag = 0 then
         pVarHeader(p + fSizeFlags and VarSizeMask - VarHeaderSize)^.prevSize := fSizeFlags and VarSizeMask;
         pVarHeader(p + fSizeFlags and VarSizeMask - VarHeaderSize)^.prevSize := fSizeFlags and VarSizeMask;
 
 
       pVarHeader(p - VarHeaderSize)^.ch.h := uint32(fSizeFlags) - UsedFlag;
       pVarHeader(p - VarHeaderSize)^.ch.h := uint32(fSizeFlags) - UsedFlag;
       pFreeVarChunk(p)^.size := fSizeFlags and VarSizeMask;
       pFreeVarChunk(p)^.size := fSizeFlags and VarSizeMask;
 
 
-      varFree.Add(@pFreeVarChunk(p)^.rbn);
+      if not Assigned(toRemove) then
+        varFree.Add(@pFreeVarChunk(p)^.rbn);
     end else
     end else
     begin
     begin
+      if Assigned(toRemove) then
+        varFree.Remove(@toRemove^.rbn);
       osChunk := p - (VarOSChunkDataOffset + VarHeaderSize);
       osChunk := p - (VarOSChunkDataOffset + VarHeaderSize);
 
 
       { Remove osChunk from varOS. }
       { Remove osChunk from varOS. }
@@ -1389,6 +1480,8 @@ type
   var
   var
     fp, p2: pointer;
     fp, p2: pointer;
     oldpsize, fSizeFlags, growby, statv: SizeUint;
     oldpsize, fSizeFlags, growby, statv: SizeUint;
+    toRemove: pFreeVarChunk;
+    adj: RbTree.pNode; { Assigned(adj) means ChangeInPlace is NOT possible. }
   begin
   begin
     if (size < MinVarHeaderAndPayload - VarHeaderSize)
     if (size < MinVarHeaderAndPayload - VarHeaderSize)
       or (size > GrowHeapSize2) { Not strictly necessary but rejects clearly wrong values early so adding headers to the size doesn’t overflow. }
       or (size > GrowHeapSize2) { Not strictly necessary but rejects clearly wrong values early so adding headers to the size doesn’t overflow. }
@@ -1402,11 +1495,12 @@ type
 
 
     oldpsize := pVarHeader(result - VarHeaderSize)^.ch.h and uint32(VarSizeMask);
     oldpsize := pVarHeader(result - VarHeaderSize)^.ch.h and uint32(VarSizeMask);
     p2 := result + oldpsize;
     p2 := result + oldpsize;
-    { (f)uture (f)ree chunk starting at p + size and having fSizeFlags will be created at the end, must exit before that if not required. }
+    { (f)uture (f)ree chunk starting at p + size and having fSizeFlags will be created at the end (replacing toRemove), must exit before that if not required. }
     if size <= oldpsize then
     if size <= oldpsize then
     begin
     begin
       { Shrink. Maybe. }
       { Shrink. Maybe. }
       fSizeFlags := oldpsize - size;
       fSizeFlags := oldpsize - size;
+      toRemove := nil;
 
 
       if pVarHeader(result - VarHeaderSize)^.ch.h and LastFlag <> 0 then
       if pVarHeader(result - VarHeaderSize)^.ch.h and LastFlag <> 0 then
       begin
       begin
@@ -1429,7 +1523,11 @@ type
         dec(used, fSizeFlags);
         dec(used, fSizeFlags);
         { Has empty chunk to the right: extend with freed space. }
         { Has empty chunk to the right: extend with freed space. }
         fSizeFlags := fSizeFlags + pFreeVarChunk(p2)^.size + pVarHeader(p2 - VarHeaderSize)^.ch.h and LastFlag;
         fSizeFlags := fSizeFlags + pFreeVarChunk(p2)^.size + pVarHeader(p2 - VarHeaderSize)^.ch.h and LastFlag;
-        varFree.Remove(@pFreeVarChunk(p2)^.rbn);
+        toRemove := p2;
+        { Free chunk size increased, so the tree position is intact if still ≤ than the next. }
+        adj := varFree.Next(@toRemove^.rbn);
+        if Assigned(adj) and (fSizeFlags and VarSizeMask <= pFreeVarChunk(pointer(adj) - FreeVarChunkRbnOffset)^.size) then
+          adj := nil;
       end;
       end;
 
 
       { Update p size. }
       { Update p size. }
@@ -1458,23 +1556,36 @@ type
       if statv > maxUsed then
       if statv > maxUsed then
         maxUsed := statv;
         maxUsed := statv;
 
 
-      varFree.Remove(@pFreeVarChunk(p2)^.rbn);
+      toRemove := p2;
       { Update p size. }
       { Update p size. }
       pVarHeader(result - VarHeaderSize)^.ch.h := uint32(size) + pVarHeader(result - VarHeaderSize)^.ch.h and (FirstFlag or UsedFlag);
       pVarHeader(result - VarHeaderSize)^.ch.h := uint32(size) + pVarHeader(result - VarHeaderSize)^.ch.h and (FirstFlag or UsedFlag);
       { No empty chunk? }
       { No empty chunk? }
       if fSizeFlags = 0 then
       if fSizeFlags = 0 then
       begin
       begin
+        varFree.Remove(@toRemove^.rbn);
         if pVarHeader(p2 - VarHeaderSize)^.ch.h and LastFlag <> 0 then
         if pVarHeader(p2 - VarHeaderSize)^.ch.h and LastFlag <> 0 then
           inc(pVarHeader(result - VarHeaderSize)^.ch.h, LastFlag)
           inc(pVarHeader(result - VarHeaderSize)^.ch.h, LastFlag)
         else
         else
           pVarHeader(result + size - VarHeaderSize)^.prevSize := size;
           pVarHeader(result + size - VarHeaderSize)^.prevSize := size;
         exit;
         exit;
       end;
       end;
+      { Free chunk size decreased, so the tree position is intact if still ≥ than the previous. }
+      adj := varFree.Prev(@toRemove^.rbn);
+      if Assigned(adj) and (fSizeFlags and VarSizeMask >= pFreeVarChunk(pointer(adj) - FreeVarChunkRbnOffset)^.size) then
+        adj := nil;
     end else
     end else
       exit(nil);
       exit(nil);
 
 
     { Format new free var chunk. }
     { Format new free var chunk. }
     fp := result + size;
     fp := result + size;
+    if Assigned(toRemove) then
+      if not Assigned(adj) then
+        varFree.ChangeInPlace(@toRemove^.rbn, @pFreeVarChunk(fp)^.rbn)
+      else
+      begin
+        varFree.Remove(@toRemove^.rbn);
+        toRemove := nil;
+      end;
     pVarHeader(fp - VarHeaderSize)^.prevSize := size;
     pVarHeader(fp - VarHeaderSize)^.prevSize := size;
   {$ifdef FPC_HAS_FEATURE_THREADING}
   {$ifdef FPC_HAS_FEATURE_THREADING}
     pVarHeader(fp - VarHeaderSize)^.threadState := @self;
     pVarHeader(fp - VarHeaderSize)^.threadState := @self;
@@ -1483,7 +1594,8 @@ type
     pFreeVarChunk(fp)^.size := fSizeFlags and VarSizeMask;
     pFreeVarChunk(fp)^.size := fSizeFlags and VarSizeMask;
     if fSizeFlags and LastFlag = 0 then
     if fSizeFlags and LastFlag = 0 then
       pVarHeader(fp + fSizeFlags and VarSizeMask - VarHeaderSize)^.prevSize := fSizeFlags and VarSizeMask;
       pVarHeader(fp + fSizeFlags and VarSizeMask - VarHeaderSize)^.prevSize := fSizeFlags and VarSizeMask;
-    varFree.Add(@pFreeVarChunk(fp)^.rbn);
+    if not Assigned(toRemove) then
+      varFree.Add(@pFreeVarChunk(fp)^.rbn);
   end;
   end;
 
 
   { If SysOSFree is available, huge chunks aren’t cached by any means.
   { If SysOSFree is available, huge chunks aren’t cached by any means.