435 lines
17 KiB
C#
435 lines
17 KiB
C#
//-----------------------------------------------------------------------
|
||
// -----------------------------------------------------------------------
|
||
// file="TestL4Behaviour"
|
||
// Project: StgSharp
|
||
// AuthorGroup: Nitload
|
||
// Copyright (c) Nitload. All rights reserved.
|
||
//
|
||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
// of this software and associated documentation files (the "Software"), to deal
|
||
// in the Software without restriction, including without limitation the rights
|
||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
// copies of the Software, and to permit persons to whom the Software is
|
||
// furnished to do so, subject to the following conditions:
|
||
//
|
||
// The above copyright notice and this permission notice shall be included in all
|
||
// copies or substantial portions of the Software.
|
||
//
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
// SOFTWARE.
|
||
//
|
||
// -----------------------------------------------------------------------
|
||
// -----------------------------------------------------------------------
|
||
using StgSharp.HighPerformance.Memory;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.CompilerServices;
|
||
|
||
namespace StgSharp
|
||
{
|
||
// ====================================================================
|
||
// TrackingPredictor
|
||
//
|
||
// 每次 Predict 返回全部地址(幂等);
|
||
// Prefetch 用可配置字节填充缓存行;
|
||
// WriteBack 记录被回写的 origin,供驱逐测试断言。
|
||
// ====================================================================
|
||
|
||
internal sealed unsafe class TrackingPredictor : IL4Predict
|
||
{
|
||
|
||
private readonly nuint[] _addresses;
|
||
|
||
public byte FillByte = 0xAB;
|
||
public readonly List<nuint> WriteBackLog = [];
|
||
|
||
public TrackingPredictor(
|
||
nuint[] addresses
|
||
)
|
||
{
|
||
_addresses = addresses;
|
||
}
|
||
|
||
public bool Predict(
|
||
Span<CacheLinePrediction> node,
|
||
out int count
|
||
)
|
||
{
|
||
count = Math.Min(_addresses.Length, node.Length);
|
||
for (int i = 0; i < count; i++) {
|
||
node[i] = new CacheLinePrediction
|
||
{
|
||
Address = _addresses[i],
|
||
MapPolicy = 0,
|
||
};
|
||
}
|
||
return count > 0;
|
||
}
|
||
|
||
public void Prefetch(
|
||
nuint origin,
|
||
ulong policy,
|
||
Span<byte> cache
|
||
)
|
||
{
|
||
cache.Fill(FillByte);
|
||
}
|
||
|
||
public void WriteBack(
|
||
nuint origin,
|
||
ulong policy,
|
||
ReadOnlySpan<byte> cache
|
||
)
|
||
{
|
||
WriteBackLog.Add(origin);
|
||
}
|
||
|
||
}
|
||
|
||
// ====================================================================
|
||
// P1 行为测试
|
||
// ====================================================================
|
||
|
||
internal static partial class Program
|
||
{
|
||
|
||
// ----------------------------------------------------------------
|
||
// 入口
|
||
// ----------------------------------------------------------------
|
||
|
||
public static void TestL4Behavior()
|
||
{
|
||
_l4FailCount = 0;
|
||
Console.WriteLine("=== L4 Behavior Tests (P1) ===");
|
||
Console.WriteLine();
|
||
|
||
L4Test_MultipleRef_SameLine();
|
||
L4Test_AgeAll_SaturatesAt3();
|
||
L4Test_Evict_RemovesStaleLines();
|
||
|
||
Console.WriteLine();
|
||
if (_l4FailCount == 0)
|
||
{
|
||
Console.WriteLine("=== ALL P1 TESTS PASSED ===");
|
||
} else
|
||
{
|
||
Console.WriteLine($"=== {_l4FailCount} TEST(S) FAILED ===");
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------------------------
|
||
// 辅助:读取 CacheLine 第一个字节
|
||
// ----------------------------------------------------------------
|
||
|
||
private static unsafe byte L4ReadFirstByte(
|
||
L4.CacheLine line
|
||
)
|
||
{
|
||
return *(byte*)line.Address;
|
||
}
|
||
|
||
// ================================================================
|
||
// P1-2 AgeAll_SaturatesAt3
|
||
//
|
||
// 目标:连续触发多次 AgeAll 后,所有已分配行的 RRPV 字节
|
||
// 均饱和在 3,不溢出、不归零。
|
||
//
|
||
// 策略:
|
||
// 1. 预取 K 条行并立即 Dispose(refCount=0,RRPV 初始为 0)
|
||
// 2. 调用 ForceAgeAll() 4 次(通过新增的 Debug 接口直接触发)
|
||
// 3. 读取 DebugSnapshot.RrpvSnapshot,断言所有已分配行 == 3
|
||
//
|
||
// 注意:AgeAll 是 private,测试依赖下方"在 L4.Debug.cs 追加
|
||
// public void DebugAgeAll()" 的接口,见文末说明。
|
||
// ================================================================
|
||
|
||
private static void L4Test_AgeAll_SaturatesAt3()
|
||
{
|
||
Console.Write("[P1-2] AgeAll_SaturatesAt3 ... ");
|
||
try
|
||
{
|
||
const int K = 4;
|
||
nuint[] addresses = MakeL4FakeAddresses(K);
|
||
using L4 l4 = new L4();
|
||
l4.PrefetchPredict = new TrackingPredictor(addresses);
|
||
|
||
// Step 1: 装填 K 条行,全部 Dispose(refCount=0)
|
||
for (int i = 0; i < K; i++)
|
||
{
|
||
L4.CacheLine line = l4.Map(addresses[i]);
|
||
line.Dispose();
|
||
}
|
||
|
||
// 装填后 RRPV 应全为 0(Map 命中时 eviction[id] = 0)
|
||
{
|
||
L4.DebugSnapshot snap = l4.GetDebugSnapshot();
|
||
for (int i = 0; i < snap.MaxId; i++) {
|
||
L4Assert(
|
||
snap.RrpvSnapshot[i] == 0,
|
||
$"{$"RrpvSnapshot[{i}] should be 0 right after Map, "}{$"got {snap.RrpvSnapshot[i]}"}");
|
||
}
|
||
}
|
||
|
||
// Step 2: AgeAll 4 次(饱和值为 3,4 次足够触顶)
|
||
for (int age = 0; age < 4; age++)
|
||
{
|
||
l4.DebugAgeAll();
|
||
}
|
||
|
||
// Step 3: 所有已分配行 RRPV == 3
|
||
{
|
||
L4.DebugSnapshot snap = l4.GetDebugSnapshot();
|
||
L4Assert(
|
||
snap.MaxId == K,
|
||
$"MaxId should be {K}, got {snap.MaxId}");
|
||
|
||
bool allSaturated = true;
|
||
for (int i = 0; i < snap.MaxId; i++)
|
||
{
|
||
if (snap.RrpvSnapshot[i] != 3)
|
||
{
|
||
L4Assert(false,
|
||
$"{$"RrpvSnapshot[{i}] == {snap.RrpvSnapshot[i]}, "}expected 3 after 4x AgeAll (saturation failure or overflow)");
|
||
allSaturated = false;
|
||
}
|
||
}
|
||
if (allSaturated) {
|
||
Console.WriteLine($"PASS (MaxId={snap.MaxId}, all RRPV==3)");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"FAIL ({ex.GetType().Name}: {ex.Message})");
|
||
_l4FailCount++;
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// P1-3 Evict_RemovesStaleLines
|
||
//
|
||
// 目标:RRPV==3 且 refCount==0 的行在 Evict 后:
|
||
// a. WriteBack 被调用,origin 是 batch1 中的地址
|
||
// b. DebugContains(evictedOrigin) == false(SwissTable 条目已移除)
|
||
// c. 该地址重新 Map 后,缓存行被重新 Prefetch(填充字节变为新值)
|
||
//
|
||
// 策略:
|
||
// 1. batch1(K 条) 装填 → Dispose → DebugAgeAll × 4(RRPV 饱和到 3)
|
||
// 2. 换 Predictor(FillByte 改为 0xCC),地址列表扩充为 batch1 + 1 条新地址
|
||
// 3. Map 新地址 → 触发 Prefetch → Prefetch 内部 Evict → WriteBack
|
||
// 4. 三项断言
|
||
// ================================================================
|
||
|
||
private static void L4Test_Evict_RemovesStaleLines()
|
||
{
|
||
Console.Write("[P1-3] Evict_RemovesStaleLines ... ");
|
||
try
|
||
{
|
||
const int K = 4;
|
||
nuint[] batch1 = MakeL4FakeAddresses(K);
|
||
|
||
TrackingPredictor predictor1 = new TrackingPredictor(batch1)
|
||
{
|
||
FillByte = 0xAA
|
||
};
|
||
using L4 l4 = new L4();
|
||
l4.PrefetchPredict = predictor1;
|
||
|
||
// ---------- Phase 1: 装填 batch1,全部 Dispose(refCount=0)----------
|
||
for (int i = 0; i < K; i++)
|
||
{
|
||
L4.CacheLine line = l4.Map(batch1[i]);
|
||
L4Assert(
|
||
L4ReadFirstByte(line) == 0xAA,
|
||
$"Initial fill should be 0xAA, got 0x{L4ReadFirstByte(line):X2}");
|
||
line.Dispose();
|
||
}
|
||
|
||
// 确认 K 条行均在 SwissTable 中
|
||
for (int i = 0; i < K; i++)
|
||
{
|
||
L4Assert(
|
||
l4.DebugContains(batch1[i]),
|
||
$"batch1[{i}] should be in SwissTable after prefetch");
|
||
}
|
||
|
||
// ---------- Phase 2: AgeAll × 4,使 RRPV 饱和到 3 ----------
|
||
for (int age = 0; age < 4; age++)
|
||
{
|
||
l4.DebugAgeAll();
|
||
}
|
||
L4.DebugSnapshot snap = l4.GetDebugSnapshot();
|
||
for (int i = 0; i < snap.MaxId; i++) {
|
||
L4Assert(
|
||
snap.RrpvSnapshot[i] == 3,
|
||
$"Pre-evict: RrpvSnapshot[{i}] should be 3, got {snap.RrpvSnapshot[i]}");
|
||
}
|
||
|
||
// ---------- Phase 3: 直接调用 DebugEvict(1),驱逐 1 条 ----------
|
||
// 此时 refCount==0 且 RRPV==3,Evict 应能找到候选
|
||
TrackingPredictor predictor2 = new TrackingPredictor(batch1)
|
||
{
|
||
FillByte = 0xCC
|
||
};
|
||
l4.PrefetchPredict = predictor2; // 换 predictor 以捕获 WriteBack 日志
|
||
|
||
int remain = l4.DebugEvict(1);
|
||
|
||
// 断言 a: remain==0,说明成功驱逐了 1 条
|
||
L4Assert(
|
||
remain == 0,
|
||
$"DebugEvict(1) should return 0 (all evicted), got remain={remain}");
|
||
|
||
// 断言 b: WriteBack 被调用,origin 属于 batch1
|
||
L4Assert(
|
||
predictor2.WriteBackLog.Count > 0,
|
||
"Evict should have called WriteBack for the evicted line");
|
||
|
||
nuint evictedOrigin = nuint.Zero;
|
||
foreach (nuint logged in predictor2.WriteBackLog)
|
||
{
|
||
foreach (nuint b1 in batch1)
|
||
{
|
||
if (logged == b1)
|
||
{
|
||
evictedOrigin = logged;
|
||
break;
|
||
}
|
||
}
|
||
if (evictedOrigin != nuint.Zero)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
L4Assert(
|
||
evictedOrigin != nuint.Zero,
|
||
$"{$"WriteBack origin should be a batch1 address, "}{($"WriteBackLog=[{string.Join(", ", predictor2.WriteBackLog)}]")}");
|
||
|
||
// 断言 c: SwissTable 中已不含该 origin
|
||
L4Assert(
|
||
!l4.DebugContains(evictedOrigin),
|
||
$"DebugContains(0x{evictedOrigin:X}) should be false after eviction");
|
||
|
||
// 断言 d: 被驱逐的地址重新 Map → 重新 Prefetch → 填充 0xCC(非旧值 0xAA)
|
||
// 注意:此时需要 predictor2 的 Predict 也包含 evictedOrigin,
|
||
// 使 Map 触发 Prefetch 时能重新装填该行
|
||
L4.CacheLine remapped = l4.Map(evictedOrigin);
|
||
byte refill = L4ReadFirstByte(remapped);
|
||
remapped.Dispose();
|
||
L4Assert(
|
||
refill == 0xCC,
|
||
$"{$"Re-mapped evicted line should be re-prefetched with 0xCC, "}{($"got 0x{refill:X2} (0xAA = stale data not evicted)")}");
|
||
|
||
if (_l4FailCount == 0) {
|
||
Console.WriteLine(
|
||
$"{($"PASS (evicted 0x{evictedOrigin:X}, remain={remain}, ")}{$"WriteBack×{predictor2.WriteBackLog.Count})"}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"FAIL ({ex.GetType().Name}: {ex.Message})");
|
||
_l4FailCount++;
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// P1-1 MultipleRef_SameLine
|
||
//
|
||
// 同一 origin 同时持有 N 个句柄时:
|
||
// a. 所有句柄 Address 非零
|
||
// b. 所有句柄 Address 相同(同一缓存行)
|
||
// c. 全部 Dispose 后 ActiveCount 归零
|
||
// d. 再次 Map 同一地址仍然成功(refCount 归零不破坏可访问性)
|
||
// ================================================================
|
||
|
||
private static void L4Test_MultipleRef_SameLine()
|
||
{
|
||
Console.Write("[P1-1] MultipleRef_SameLine ... ");
|
||
const int N = 8;
|
||
try
|
||
{
|
||
nuint[] addresses = MakeL4FakeAddresses(1);
|
||
using L4 l4 = new L4();
|
||
l4.PrefetchPredict = new TrackingPredictor(addresses);
|
||
|
||
nuint target = addresses[0];
|
||
|
||
// ref struct 不能形成数组,展开为 N 个具名局部变量同时持有
|
||
L4.CacheLine l0 = l4.Map(target);
|
||
L4.CacheLine l1 = l4.Map(target);
|
||
L4.CacheLine l2 = l4.Map(target);
|
||
L4.CacheLine l3 = l4.Map(target);
|
||
L4.CacheLine l4b = l4.Map(target);
|
||
L4.CacheLine l5 = l4.Map(target);
|
||
L4.CacheLine l6 = l4.Map(target);
|
||
L4.CacheLine l7 = l4.Map(target);
|
||
|
||
// a. 所有句柄 Address 非零
|
||
L4Assert(l0.Address != nuint.Zero, "l0.Address should not be zero");
|
||
L4Assert(l1.Address != nuint.Zero, "l1.Address should not be zero");
|
||
L4Assert(l2.Address != nuint.Zero, "l2.Address should not be zero");
|
||
L4Assert(l3.Address != nuint.Zero, "l3.Address should not be zero");
|
||
L4Assert(l4b.Address != nuint.Zero, "l4b.Address should not be zero");
|
||
L4Assert(l5.Address != nuint.Zero, "l5.Address should not be zero");
|
||
L4Assert(l6.Address != nuint.Zero, "l6.Address should not be zero");
|
||
L4Assert(l7.Address != nuint.Zero, "l7.Address should not be zero");
|
||
|
||
// b. 所有句柄 Address 相同(同一缓存行)
|
||
nuint first = l0.Address;
|
||
L4Assert(l1.Address == first, $"l1 0x{l1.Address:X} != l0 0x{first:X}");
|
||
L4Assert(l2.Address == first, $"l2 0x{l2.Address:X} != l0 0x{first:X}");
|
||
L4Assert(l3.Address == first, $"l3 0x{l3.Address:X} != l0 0x{first:X}");
|
||
L4Assert(l4b.Address == first, $"l4b 0x{l4b.Address:X} != l0 0x{first:X}");
|
||
L4Assert(l5.Address == first, $"l5 0x{l5.Address:X} != l0 0x{first:X}");
|
||
L4Assert(l6.Address == first, $"l6 0x{l6.Address:X} != l0 0x{first:X}");
|
||
L4Assert(l7.Address == first, $"l7 0x{l7.Address:X} != l0 0x{first:X}");
|
||
|
||
// c. 持有期间 ActiveCount == N
|
||
{
|
||
L4.DebugSnapshot snap = l4.GetDebugSnapshot();
|
||
L4Assert(
|
||
snap.ActiveCount == N,
|
||
$"ActiveCount should be {N} while {N} handles live, got {snap.ActiveCount}");
|
||
}
|
||
|
||
// 全部 Dispose
|
||
l0.Dispose();
|
||
l1.Dispose();
|
||
l2.Dispose();
|
||
l3.Dispose();
|
||
l4b.Dispose();
|
||
l5.Dispose();
|
||
l6.Dispose();
|
||
l7.Dispose();
|
||
|
||
// c(续). Dispose 后 ActiveCount 归零
|
||
{
|
||
L4.DebugSnapshot snap = l4.GetDebugSnapshot();
|
||
L4Assert(
|
||
snap.ActiveCount == 0,
|
||
$"ActiveCount should be 0 after all disposed, got {snap.ActiveCount}");
|
||
}
|
||
|
||
// d. 再次 Map 仍然成功
|
||
L4.CacheLine again = l4.Map(target);
|
||
L4Assert(again.Address != nuint.Zero,
|
||
"Map after all refs disposed should still return valid address");
|
||
again.Dispose();
|
||
|
||
Console.WriteLine("PASS");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"FAIL ({ex.GetType().Name}: {ex.Message})");
|
||
_l4FailCount++;
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|