diff --git a/Assets/Scripts/Simulation/TlbCache.cs b/Assets/Scripts/Simulation/TlbCache.cs new file mode 100644 index 0000000..29ae0c2 --- /dev/null +++ b/Assets/Scripts/Simulation/TlbCache.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; + +namespace VMdemo.Simulation +{ + public class TlbCache + { + private readonly Dictionary> _index; + private readonly LinkedList _lruList; + + public TlbCache(int capacity) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), "TLB 容量必须是正整数。"); + } + + Capacity = capacity; + _index = new Dictionary>(capacity); + _lruList = new LinkedList(); + } + + public int Capacity { get; } + public int Count => _index.Count; + public int HitCount { get; private set; } + public int MissCount { get; private set; } + + public bool Lookup(ulong vpn, out ulong pfn) + { + return TryLookup(vpn, out pfn); + } + + public bool TryLookup(ulong vpn, out ulong pfn) + { + if (_index.TryGetValue(vpn, out var node) && node.Value.IsValid) + { + MoveToMostRecent(node); + pfn = node.Value.Pfn; + HitCount++; + return true; + } + + pfn = 0UL; + MissCount++; + return false; + } + + public void InsertOrUpdate(ulong vpn, ulong pfn) + { + if (_index.TryGetValue(vpn, out var existingNode)) + { + existingNode.Value = existingNode.Value.WithPfn(pfn); + MoveToMostRecent(existingNode); + return; + } + + var newNode = new LinkedListNode(new TlbEntry(vpn, pfn)); + _lruList.AddFirst(newNode); + _index[vpn] = newNode; + + if (_index.Count > Capacity) + { + EvictLeastRecent(); + } + } + + public bool Remove(ulong vpn) + { + if (!_index.TryGetValue(vpn, out var node)) + { + return false; + } + + _lruList.Remove(node); + _index.Remove(vpn); + return true; + } + + public void Clear() + { + _lruList.Clear(); + _index.Clear(); + HitCount = 0; + MissCount = 0; + } + + public IReadOnlyList GetEntriesMostRecentFirst() + { + var snapshot = new List(_index.Count); + for (var node = _lruList.First; node != null; node = node.Next) + { + snapshot.Add(node.Value); + } + + return snapshot; + } + + private void MoveToMostRecent(LinkedListNode node) + { + if (node.List != _lruList || _lruList.First == node) + { + return; + } + + _lruList.Remove(node); + _lruList.AddFirst(node); + } + + private void EvictLeastRecent() + { + var leastRecentNode = _lruList.Last; + if (leastRecentNode == null) + { + return; + } + + _lruList.RemoveLast(); + _index.Remove(leastRecentNode.Value.Vpn); + } + } +} diff --git a/Assets/Scripts/Simulation/TlbCache.cs.meta b/Assets/Scripts/Simulation/TlbCache.cs.meta new file mode 100644 index 0000000..b48a1d9 --- /dev/null +++ b/Assets/Scripts/Simulation/TlbCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80f194356d342d24d8a3a8bfc9f22599 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Simulation/TlbEntry.cs b/Assets/Scripts/Simulation/TlbEntry.cs new file mode 100644 index 0000000..1ceb515 --- /dev/null +++ b/Assets/Scripts/Simulation/TlbEntry.cs @@ -0,0 +1,29 @@ +using System; + +namespace VMdemo.Simulation +{ + [Serializable] + public readonly struct TlbEntry + { + public TlbEntry(ulong vpn, ulong pfn, bool isValid = true) + { + Vpn = vpn; + Pfn = pfn; + IsValid = isValid; + } + + public ulong Vpn { get; } + public ulong Pfn { get; } + public bool IsValid { get; } + + public TlbEntry WithPfn(ulong pfn) + { + return new TlbEntry(Vpn, pfn, true); + } + + public override string ToString() + { + return $"VPN={Vpn}, PFN={Pfn}, Valid={IsValid}"; + } + } +} diff --git a/Assets/Scripts/Simulation/TlbEntry.cs.meta b/Assets/Scripts/Simulation/TlbEntry.cs.meta new file mode 100644 index 0000000..e4b7386 --- /dev/null +++ b/Assets/Scripts/Simulation/TlbEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc1daa201fa9e844a83e62ff9feb5ae6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/Step3TlbCacheTests.cs b/Assets/Tests/EditMode/Step3TlbCacheTests.cs new file mode 100644 index 0000000..6a631b2 --- /dev/null +++ b/Assets/Tests/EditMode/Step3TlbCacheTests.cs @@ -0,0 +1,130 @@ +using NUnit.Framework; +using VMdemo.Simulation; + +namespace VMdemo.Tests.EditMode +{ + public class Step3TlbCacheTests + { + [Test] + public void Constructor_CapacityLessThanOne_Throws() + { + Assert.Throws(() => new TlbCache(0)); + Assert.Throws(() => new TlbCache(-1)); + } + + [Test] + public void TryLookup_Miss_ReturnsFalseAndCountsMiss() + { + var cache = new TlbCache(2); + + var hit = cache.TryLookup(10UL, out var pfn); + + Assert.IsFalse(hit); + Assert.That(pfn, Is.EqualTo(0UL)); + Assert.That(cache.HitCount, Is.EqualTo(0)); + Assert.That(cache.MissCount, Is.EqualTo(1)); + } + + [Test] + public void InsertOrUpdate_AndLookup_HitReturnsMappedPfn() + { + var cache = new TlbCache(2); + cache.InsertOrUpdate(10UL, 100UL); + + var hit = cache.TryLookup(10UL, out var pfn); + + Assert.IsTrue(hit); + Assert.That(pfn, Is.EqualTo(100UL)); + Assert.That(cache.HitCount, Is.EqualTo(1)); + Assert.That(cache.MissCount, Is.EqualTo(0)); + } + + [Test] + public void InsertOrUpdate_ExistingEntry_UpdatesPfnAndMovesToMostRecent() + { + var cache = new TlbCache(3); + cache.InsertOrUpdate(1UL, 11UL); + cache.InsertOrUpdate(2UL, 22UL); + cache.InsertOrUpdate(3UL, 33UL); + + cache.InsertOrUpdate(1UL, 111UL); + + var entries = cache.GetEntriesMostRecentFirst(); + + Assert.That(entries.Count, Is.EqualTo(3)); + Assert.That(entries[0].Vpn, Is.EqualTo(1UL)); + Assert.That(entries[0].Pfn, Is.EqualTo(111UL)); + Assert.That(entries[1].Vpn, Is.EqualTo(3UL)); + Assert.That(entries[2].Vpn, Is.EqualTo(2UL)); + } + + [Test] + public void OverCapacity_EvictsLeastRecentlyUsedEntry() + { + var cache = new TlbCache(3); + cache.InsertOrUpdate(1UL, 11UL); + cache.InsertOrUpdate(2UL, 22UL); + cache.InsertOrUpdate(3UL, 33UL); + + cache.TryLookup(1UL, out _); // refresh 1 as MRU + cache.InsertOrUpdate(4UL, 44UL); // should evict 2 + + Assert.That(cache.Count, Is.EqualTo(3)); + Assert.IsFalse(cache.TryLookup(2UL, out _)); + Assert.IsTrue(cache.TryLookup(1UL, out var pfn1)); + Assert.That(pfn1, Is.EqualTo(11UL)); + + var entries = cache.GetEntriesMostRecentFirst(); + Assert.That(entries[0].Vpn, Is.EqualTo(1UL)); + Assert.That(entries[1].Vpn, Is.EqualTo(4UL)); + Assert.That(entries[2].Vpn, Is.EqualTo(3UL)); + } + + [Test] + public void Remove_ExistingAndMissingKeys_BehaveAsExpected() + { + var cache = new TlbCache(2); + cache.InsertOrUpdate(5UL, 55UL); + + var removedFirst = cache.Remove(5UL); + var removedSecond = cache.Remove(5UL); + + Assert.IsTrue(removedFirst); + Assert.IsFalse(removedSecond); + Assert.That(cache.Count, Is.EqualTo(0)); + } + + [Test] + public void Clear_ResetsEntriesAndCounters() + { + var cache = new TlbCache(2); + cache.InsertOrUpdate(1UL, 10UL); + cache.InsertOrUpdate(2UL, 20UL); + cache.TryLookup(1UL, out _); + cache.TryLookup(99UL, out _); + + cache.Clear(); + + Assert.That(cache.Count, Is.EqualTo(0)); + Assert.That(cache.HitCount, Is.EqualTo(0)); + Assert.That(cache.MissCount, Is.EqualTo(0)); + Assert.That(cache.GetEntriesMostRecentFirst().Count, Is.EqualTo(0)); + } + + [Test] + public void ContinuousLookup_TracksHitAndMissCountsAccurately() + { + var cache = new TlbCache(2); + cache.InsertOrUpdate(1UL, 10UL); + cache.InsertOrUpdate(2UL, 20UL); + + cache.TryLookup(1UL, out _); // hit + cache.TryLookup(3UL, out _); // miss + cache.TryLookup(2UL, out _); // hit + cache.TryLookup(3UL, out _); // miss + + Assert.That(cache.HitCount, Is.EqualTo(2)); + Assert.That(cache.MissCount, Is.EqualTo(2)); + } + } +} diff --git a/Assets/Tests/EditMode/Step3TlbCacheTests.cs.meta b/Assets/Tests/EditMode/Step3TlbCacheTests.cs.meta new file mode 100644 index 0000000..3704626 --- /dev/null +++ b/Assets/Tests/EditMode/Step3TlbCacheTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 971f2b92f0eedec46b9cfb3f60199340 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/doc/MVP-TODO.md b/doc/MVP-TODO.md index d00abb2..0070421 100644 --- a/doc/MVP-TODO.md +++ b/doc/MVP-TODO.md @@ -83,20 +83,20 @@ ### TODO -- [ ] 新建 `TlbEntry.cs`:`VPN -> PFN` 映射及有效位 -- [ ] 新建 `TlbCache.cs`:查询、更新、淘汰 -- [ ] 使用 `Dictionary + LinkedList` 实现 O(1) 级 LRU +- [x] 新建 `TlbEntry.cs`:`VPN -> PFN` 映射及有效位 +- [x] 新建 `TlbCache.cs`:查询、更新、淘汰 +- [x] 使用 `Dictionary + LinkedList` 实现 O(1) 级 LRU ### 行为要求 -- [ ] `Lookup(vpn)`:命中返回 PFN,未命中返回失败 -- [ ] `InsertOrUpdate(vpn, pfn)`:已存在则更新并置为最近使用 -- [ ] 超出容量时淘汰最久未使用项 +- [x] `Lookup(vpn)`:命中返回 PFN,未命中返回失败 +- [x] `InsertOrUpdate(vpn, pfn)`:已存在则更新并置为最近使用 +- [x] 超出容量时淘汰最久未使用项 ### 完成标准 -- [ ] 构造用例可验证 LRU 淘汰顺序正确 -- [ ] 连续访问时命中统计准确 +- [x] 构造用例可验证 LRU 淘汰顺序正确 +- [x] 连续访问时命中统计准确 ## 5. Step 4 - 二级页表 + 缺页 + FIFO @@ -207,4 +207,4 @@ - [x] 先完成 Step 1(参数模型 + 校验) - [x] 紧接 Step 2(地址拆分) -- [ ] 当天收尾前完成 Step 3(TLB LRU 最小可运行) +- [x] 当天收尾前完成 Step 3(TLB LRU 最小可运行)