[GpuMemory] Scan possibly CPU-dirty blocks 64 at a time

ForEachPossiblyCpuDirtyRange combined a region lookup and a summary read for
every 4 MiB block of mapped memory on each device-address sweep. A bitset of
blocks that have a region and the dirty summary words are now scanned a word
at a time, with the same runs as before.
This commit is contained in:
ParantezTech 2026-10-02 18:47:22 +03:00
parent 87c5195ba3
commit f2d55afe69
3 changed files with 59 additions and 6 deletions

View File

@ -33,4 +33,6 @@ public sealed class CpuDirtySummary
}
public bool IsDirty(ulong block) => (Volatile.Read(ref _words[block / WordBits]) & (1L << (int)(block % WordBits))) != 0;
public ulong Word(int index) => (ulong)Volatile.Read(ref _words[index]);
}

View File

@ -15,6 +15,7 @@ public sealed class GuestPageTracker
private readonly PageGuard _pages;
private readonly TrackedRegion?[] _regions = new TrackedRegion?[TrackerLayout.BlockCount];
private readonly long[] _presentRegions = new long[(TrackerLayout.BlockCount + 63) / 64];
private readonly object _regionGate = new();
private readonly CpuDirtySummary _cpuDirtySummary = new();
@ -448,19 +449,42 @@ public sealed class GuestPageTracker
var last = (end - 1) / BlockBytes;
var runStart = 0UL;
var inRun = false;
for (var index = vaddr / BlockBytes; index <= last; index++)
var index = vaddr / BlockBytes;
var loadedWord = -1;
var possiblyDirty = 0UL;
while (index <= last)
{
var possiblyDirty = Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index);
if (possiblyDirty && !inRun)
var word = (int)(index / 64);
if (word != loadedWord)
{
runStart = Math.Max(vaddr, index * BlockBytes);
inRun = true;
loadedWord = word;
possiblyDirty = ~(ulong)Volatile.Read(ref _presentRegions[word]) | _cpuDirtySummary.Word(word);
}
else if (!possiblyDirty && inRun)
var shift = (int)(index % 64);
var pending = (inRun ? ~possiblyDirty : possiblyDirty) >> shift;
if (pending == 0)
{
index = ((ulong)word + 1) * 64;
continue;
}
index += (ulong)System.Numerics.BitOperations.TrailingZeroCount(pending);
if (index > last)
{
break;
}
if (inRun)
{
visit(runStart, index * BlockBytes - runStart);
inRun = false;
}
else
{
runStart = Math.Max(vaddr, index * BlockBytes);
inRun = true;
}
}
if (inRun)
@ -524,6 +548,7 @@ public sealed class GuestPageTracker
var created = new TrackedRegion(_pages, index * BlockBytes, _cpuDirtySummary);
Volatile.Write(ref _regions[index], created);
Interlocked.Or(ref _presentRegions[index / 64], 1L << (int)(index % 64));
return created;
}
}

View File

@ -238,6 +238,32 @@ public sealed class GuestPageTrackerTests : IDisposable
Release(address, Region * 3);
}
[NativePageProtectionFact]
public void PossiblyCpuDirtyRangesCrossSummaryWords()
{
var address = AllocateAligned(Region * 2, Region * 64);
List<(ulong Address, ulong Size)> Runs(ulong start, ulong size)
{
var runs = new List<(ulong Address, ulong Size)>();
_tracker.ForEachPossiblyCpuDirtyRange(start, size, (runAddress, runSize) => runs.Add((runAddress, runSize)));
return runs;
}
_tracker.ForEachUploadRange(address, Region * 2, false, NoRange, NoUpload, preserveCpuWriteHotPages: false);
Assert.Equal([(address - Region * 2 + Page, Region * 2 - Page)], Runs(address - Region * 2 + Page, Region * 4 - Page * 2));
_tracker.MarkCpuDirtyPages(address + Region + Page, 8);
Assert.Equal([(address - Region * 2, Region * 2), (address + Region, Region - Page)], Runs(address - Region * 2, Region * 4 - Page));
Assert.Equal([(address + Region, Region)], Runs(address, Region * 2));
Assert.Equal([(address + Region, Region * 64)], Runs(address, Region * 65));
_tracker.ForEachUploadRange(address, Region * 2, false, NoRange, NoUpload, preserveCpuWriteHotPages: false);
Assert.Empty(Runs(address, Region * 2));
Assert.Equal([(address + Region * 2, Region * 63)], Runs(address, Region * 65));
_tracker.UntrackMemory(address, Region * 2);
Release(address, Region * 2);
}
[NativePageProtectionFact]
public void RangeInvalidationBatchesOwnershipTransferAcrossRegions()
{