From 84dc18a5b17edde8151d26572bc12b54b3c86df5 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 19 May 2024 20:39:45 -0700 Subject: [PATCH 01/13] lock on read --- .../Lru/ConcurrentLruTests.cs | 13 ++++++ BitFaster.Caching.UnitTests/TypePropsTests.cs | 31 +++++++++++++ BitFaster.Caching/Lru/ConcurrentLruCore.cs | 14 +++++- BitFaster.Caching/TypeProps.cs | 46 +++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 BitFaster.Caching.UnitTests/TypePropsTests.cs create mode 100644 BitFaster.Caching/TypeProps.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index 1a9b32f1..e52015d3 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -773,6 +773,19 @@ public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() value.Should().Be("2"); } + [Fact] + public void WhenKeyExistsAddOrUpdateGuidUpdatesExistingItem() + { + var lru2 = new ConcurrentLru(1, capacity, EqualityComparer.Default); + + var b = new byte[8]; + lru2.AddOrUpdate(1, new Guid(1, 0, 0, b)); + lru2.AddOrUpdate(1, new Guid(2, 0, 0, b)); + + lru2.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(new Guid(2, 0, 0, b)); + } + [Fact] public void WhenKeyExistsAddOrUpdateDisposesOldValue() { diff --git a/BitFaster.Caching.UnitTests/TypePropsTests.cs b/BitFaster.Caching.UnitTests/TypePropsTests.cs new file mode 100644 index 00000000..a02d9c80 --- /dev/null +++ b/BitFaster.Caching.UnitTests/TypePropsTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests +{ + public class TypePropsTests + { + private static readonly MethodInfo method = typeof(TypePropsTests).GetMethod(nameof(TypePropsTests.IsWriteAtomic), BindingFlags.NonPublic | BindingFlags.Static); + + [Theory] + [InlineData(typeof(object), true)] + [InlineData(typeof(IntPtr), true)] + [InlineData(typeof(UIntPtr), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(long), true)] // this is only expected to pass on 64bit platforms + [InlineData(typeof(Guid), false)] + public void Test(Type argType, bool expected) + { + var isWriteAtomic = method.MakeGenericMethod(argType); + + isWriteAtomic.Invoke(null, null).Should().BeOfType().Which.Should().Be(expected); + } + + private static bool IsWriteAtomic() + { + return TypeProps.IsWriteAtomic; + } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 0438ffd1..ec6deed1 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -179,7 +179,19 @@ private bool GetOrDiscard(I item, [MaybeNullWhen(false)] out V value) return false; } - value = item.Value; + if (TypeProps.IsWriteAtomic) + { + value = item.Value; + } + else + { + // prevent torn read for non-atomic types + lock (item) + { + value = item.Value; + } + } + this.itemPolicy.Touch(item); this.telemetryPolicy.IncrementHit(); return true; diff --git a/BitFaster.Caching/TypeProps.cs b/BitFaster.Caching/TypeProps.cs new file mode 100644 index 00000000..0303149c --- /dev/null +++ b/BitFaster.Caching/TypeProps.cs @@ -0,0 +1,46 @@ +using System; + +namespace BitFaster.Caching +{ + // https://source.dot.net/#System.Collections.Concurrent/System/Collections/Concurrent/ConcurrentDictionary.cs,2293 + internal static class TypeProps + { + /// Whether T's type can be written atomically (i.e., with no danger of torn reads). + internal static readonly bool IsWriteAtomic = IsWriteAtomicPrivate(); + + private static bool IsWriteAtomicPrivate() + { + // Section 12.6.6 of ECMA CLI explains which types can be read and written atomically without + // the risk of tearing. See https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf + + if (!typeof(T).IsValueType || + typeof(T) == typeof(IntPtr) || + typeof(T) == typeof(UIntPtr)) + { + return true; + } + + switch (Type.GetTypeCode(typeof(T))) + { + case TypeCode.Boolean: + case TypeCode.Byte: + case TypeCode.Char: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.SByte: + case TypeCode.Single: + case TypeCode.UInt16: + case TypeCode.UInt32: + return true; + + case TypeCode.Double: + case TypeCode.Int64: + case TypeCode.UInt64: + return IntPtr.Size == 8; + + default: + return false; + } + } + } +} From a1762d67cd8efec4e7cc083f02281d580a7f67fa Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 19 May 2024 21:04:43 -0700 Subject: [PATCH 02/13] bench --- .../Lru/LruJustGetOrAddGuid.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs diff --git a/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs b/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs new file mode 100644 index 00000000..21edcc2c --- /dev/null +++ b/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs @@ -0,0 +1,107 @@ +using Benchly; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BitFaster.Caching.Lfu; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Scheduler; +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace BitFaster.Caching.Benchmarks +{ + +#if Windows + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] + [SimpleJob(RuntimeMoniker.Net48)] +#endif + [SimpleJob(RuntimeMoniker.Net60)] + [MemoryDiagnoser(displayGenColumns: false)] + // [HardwareCounters(HardwareCounter.LlcMisses, HardwareCounter.CacheMisses)] // Requires Admin https://adamsitnik.com/Hardware-Counters-Diagnoser/ + // [ThreadingDiagnoser] // Requires .NET Core + [HideColumns("Job", "Median", "RatioSD", "Alloc Ratio")] + [ColumnChart(Title= "Lookup Latency ({JOB})", Output = OutputMode.PerJob, Colors = "darkslategray,royalblue,royalblue,#ffbf00,indianred,indianred")] + public class LruJustGetOrAddGuid + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + private static readonly FastConcurrentLru fastConcurrentLru = new FastConcurrentLru(8, 9, EqualityComparer.Default); + + + private static readonly BackgroundThreadScheduler background = new BackgroundThreadScheduler(); + private static readonly ConcurrentLfu concurrentLfu = new ConcurrentLfu(1, 9, background, EqualityComparer.Default); + + private static readonly int key = 1; + private static System.Runtime.Caching.MemoryCache memoryCache = System.Runtime.Caching.MemoryCache.Default; + + Microsoft.Extensions.Caching.Memory.MemoryCache exMemoryCache + = new Microsoft.Extensions.Caching.Memory.MemoryCache(new MemoryCacheOptionsAccessor()); + + private static readonly byte[] b = new byte[8]; + + [GlobalSetup] + public void GlobalSetup() + { + memoryCache.Set(key.ToString(), new Guid(key, 0, 0, b), new System.Runtime.Caching.CacheItemPolicy()); + exMemoryCache.Set(key, new Guid(key, 0, 0, b)); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + background.Dispose(); + } + + [Benchmark(Baseline = true)] + public Guid ConcurrentDictionary() + { + Func func = x => new Guid(x, 0, 0, b); + return dictionary.GetOrAdd(1, func); + } + + [Benchmark()] + public Guid FastConcurrentLru() + { + Func func = x => new Guid(x, 0, 0, b); + return fastConcurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public Guid ConcurrentLru() + { + Func func = x => new Guid(x, 0, 0, b); + return concurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public Guid ConcurrentLfu() + { + Func func = x => new Guid(x, 0, 0, b); + return concurrentLfu.GetOrAdd(1, func); + } + + [Benchmark()] + public Guid RuntimeMemoryCacheGet() + { + return (Guid)memoryCache.Get("1"); + } + + [Benchmark()] + public Guid ExtensionsMemoryCacheGet() + { + return (Guid)exMemoryCache.Get(1); + } + + public class MemoryCacheOptionsAccessor + : Microsoft.Extensions.Options.IOptions + { + private readonly MemoryCacheOptions options = new MemoryCacheOptions(); + + public MemoryCacheOptions Value => this.options; + + } + } +} From be8f0ed835b24bf6303adb470ec531d4c3415ddb Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 19 May 2024 21:07:12 -0700 Subject: [PATCH 03/13] fix title --- BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs b/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs index 21edcc2c..ab613741 100644 --- a/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs +++ b/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs @@ -22,7 +22,7 @@ namespace BitFaster.Caching.Benchmarks // [HardwareCounters(HardwareCounter.LlcMisses, HardwareCounter.CacheMisses)] // Requires Admin https://adamsitnik.com/Hardware-Counters-Diagnoser/ // [ThreadingDiagnoser] // Requires .NET Core [HideColumns("Job", "Median", "RatioSD", "Alloc Ratio")] - [ColumnChart(Title= "Lookup Latency ({JOB})", Output = OutputMode.PerJob, Colors = "darkslategray,royalblue,royalblue,#ffbf00,indianred,indianred")] + [ColumnChart(Title= "Guid Lookup Latency ({JOB})", Output = OutputMode.PerJob, Colors = "darkslategray,royalblue,royalblue,#ffbf00,indianred,indianred")] public class LruJustGetOrAddGuid { private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); From 87fcd06c3e991314ed3ca25387403b19b62b7850 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 20 May 2024 12:59:29 -0700 Subject: [PATCH 04/13] seqlock --- .../Lru/LruJustGetOrAddGuid.cs | 1 + BitFaster.Caching/Lru/ConcurrentLruCore.cs | 16 +++++--- BitFaster.Caching/Lru/LruItem.cs | 38 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs b/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs index ab613741..78602e6c 100644 --- a/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs +++ b/BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAddGuid.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; namespace BitFaster.Caching.Benchmarks { diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index ec6deed1..1cc04f8a 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -186,10 +186,7 @@ private bool GetOrDiscard(I item, [MaybeNullWhen(false)] out V value) else { // prevent torn read for non-atomic types - lock (item) - { - value = item.Value; - } + value = item.SeqLockRead(); } this.itemPolicy.Touch(item); @@ -394,7 +391,16 @@ public bool TryUpdate(K key, V value) if (!existing.WasRemoved) { V oldValue = existing.Value; - existing.Value = value; + + if (TypeProps.IsWriteAtomic) + { + existing.Value = value; + } + else + { + existing.SeqLockWrite(value); + } + this.itemPolicy.Update(existing); // backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER diff --git a/BitFaster.Caching/Lru/LruItem.cs b/BitFaster.Caching/Lru/LruItem.cs index 56944316..d4f40c4a 100644 --- a/BitFaster.Caching/Lru/LruItem.cs +++ b/BitFaster.Caching/Lru/LruItem.cs @@ -1,4 +1,6 @@  +using System.Threading; + namespace BitFaster.Caching.Lru { /// @@ -11,6 +13,8 @@ public class LruItem private volatile bool wasAccessed; private volatile bool wasRemoved; + private int sequence; + /// /// Initializes a new instance of the LruItem class with the specified key and value. /// @@ -49,5 +53,39 @@ public bool WasRemoved get => this.wasRemoved; set => this.wasRemoved = value; } + + internal V SeqLockRead() + { + var spin = new SpinWait(); + while (true) + { + var start = Volatile.Read(ref this.sequence); + + if ((start & 1) == 1) + { + // A write is in progress. Back off and keep spinning. + spin.SpinOnce(); + continue; + } + + V copy = this.Value; + + var end = Volatile.Read(ref this.sequence); + if (start == end) + { + return copy; + } + } + } + + // Note: item should be locked while invoking this method. Multiple writer threads are not supported. + internal void SeqLockWrite(V value) + { + Interlocked.Increment(ref sequence); + + this.Value = value; + + Interlocked.Increment(ref sequence); + } } } From 873a61489a48ff1d55c346cd2a694c020e7a6e3e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 20 May 2024 14:36:19 -0700 Subject: [PATCH 05/13] add repro program --- .../Lru/LruItemMemoryLayoutDumps.cs | 75 ++++++++-------- Tools/SeqLock/LruItem.cs | 87 +++++++++++++++++++ Tools/SeqLock/Program.cs | 29 +++++++ Tools/SeqLock/SeqLock.csproj | 11 +++ Tools/SeqLock/TornProgram.cs | 40 +++++++++ Tools/SeqLock/TornProgramSeqLock.cs | 39 +++++++++ Tools/Tools.sln | 6 ++ 7 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 Tools/SeqLock/LruItem.cs create mode 100644 Tools/SeqLock/Program.cs create mode 100644 Tools/SeqLock/SeqLock.csproj create mode 100644 Tools/SeqLock/TornProgram.cs create mode 100644 Tools/SeqLock/TornProgramSeqLock.cs diff --git a/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs b/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs index b80ddc45..a46b5f30 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs @@ -1,5 +1,4 @@ -using System; -using BitFaster.Caching.Lru; +using BitFaster.Caching.Lru; using ObjectLayoutInspector; using Xunit; using Xunit.Abstractions; @@ -16,22 +15,24 @@ public LruItemMemoryLayoutDumps(ITestOutputHelper testOutputHelper) } //Type layout for 'LruItem`2' - //Size: 24 bytes.Paddings: 6 bytes(%25 of empty space) - //|===============================================| - //| Object Header(8 bytes) | - //|-----------------------------------------------| - //| Method Table Ptr(8 bytes) | - //|===============================================| - //| 0-7: Object Key(8 bytes) | - //|-----------------------------------------------| - //| 8-15: Object k__BackingField(8 bytes) | - //|-----------------------------------------------| - //| 16: Boolean wasAccessed(1 byte) | - //|-----------------------------------------------| - //| 17: Boolean wasRemoved(1 byte) | - //|-----------------------------------------------| - //| 18-23: padding(6 bytes) | - //|===============================================| + //Size: 24 bytes. Paddings: 2 bytes (%8 of empty space) + //|================================================| + //| Object Header (8 bytes) | + //|------------------------------------------------| + //| Method Table Ptr (8 bytes) | + //|================================================| + //| 0-7: Object Key (8 bytes) | + //|------------------------------------------------| + //| 8-15: Object k__BackingField (8 bytes) | + //|------------------------------------------------| + //| 16-19: Int32 sequence (4 bytes) | + //|------------------------------------------------| + //| 20: Boolean wasAccessed (1 byte) | + //|------------------------------------------------| + //| 21: Boolean wasRemoved (1 byte) | + //|------------------------------------------------| + //| 22-23: padding (2 bytes) | + //|================================================| [Fact] public void DumpLruItem() { @@ -40,24 +41,26 @@ public void DumpLruItem() } //Type layout for 'LongTickCountLruItem`2' - //Size: 32 bytes.Paddings: 6 bytes(%18 of empty space) - //|==================================================| - //| Object Header(8 bytes) | - //|--------------------------------------------------| - //| Method Table Ptr(8 bytes) | - //|==================================================| - //| 0-7: Object Key(8 bytes) | - //|--------------------------------------------------| - //| 8-15: Object k__BackingField(8 bytes) | - //|--------------------------------------------------| - //| 16: Boolean wasAccessed(1 byte) | - //|--------------------------------------------------| - //| 17: Boolean wasRemoved(1 byte) | - //|--------------------------------------------------| - //| 18-23: padding(6 bytes) | - //|--------------------------------------------------| - //| 24-31: Int64 k__BackingField(8 bytes) | - //|==================================================| + //Size: 32 bytes. Paddings: 2 bytes (%6 of empty space) + //|===================================================| + //| Object Header (8 bytes) | + //|---------------------------------------------------| + //| Method Table Ptr (8 bytes) | + //|===================================================| + //| 0-7: Object Key (8 bytes) | + //|---------------------------------------------------| + //| 8-15: Object k__BackingField (8 bytes) | + //|---------------------------------------------------| + //| 16-19: Int32 sequence (4 bytes) | + //|---------------------------------------------------| + //| 20: Boolean wasAccessed (1 byte) | + //|---------------------------------------------------| + //| 21: Boolean wasRemoved (1 byte) | + //|---------------------------------------------------| + //| 22-23: padding (2 bytes) | + //|---------------------------------------------------| + //| 24-31: Int64 k__BackingField (8 bytes) | + //|===================================================| [Fact] public void DumpLongTickCountLruItem() { diff --git a/Tools/SeqLock/LruItem.cs b/Tools/SeqLock/LruItem.cs new file mode 100644 index 00000000..7a8a5133 --- /dev/null +++ b/Tools/SeqLock/LruItem.cs @@ -0,0 +1,87 @@ +namespace SeqLock +{ + public class LruItem + { + private volatile bool wasAccessed; + private volatile bool wasRemoved; + + private int sequence; + + /// + /// Initializes a new instance of the LruItem class with the specified key and value. + /// + /// The key. + /// The value. + public LruItem(K k, V v) + { + this.Key = k; + this.Value = v; + } + + /// + /// Gets the key. + /// + public readonly K Key; + + /// + /// Gets or sets the value. + /// + public V Value { get; set; } + + /// + /// Gets or sets a value indicating whether the item was accessed. + /// + public bool WasAccessed + { + get => this.wasAccessed; + set => this.wasAccessed = value; + } + + /// + /// Gets or sets a value indicating whether the item was removed. + /// + public bool WasRemoved + { + get => this.wasRemoved; + set => this.wasRemoved = value; + } + + public V Read() + { + //return this.Value; + + var spin = new SpinWait(); + while (true) + { + var start = Volatile.Read(ref this.sequence); + + if ((start & 1) == 1) + { + // A write is in progress. Back off and keep spinning. + spin.SpinOnce(); + continue; + } + + V copy = this.Value; + + var end = Volatile.Read(ref this.sequence); + if (start == end) + { + return copy; + } + } + } + + public void Write(V value) + { + lock (this) + { + Interlocked.Increment(ref sequence); + + this.Value = value; + + Interlocked.Increment(ref sequence); + } + } + } +} diff --git a/Tools/SeqLock/Program.cs b/Tools/SeqLock/Program.cs new file mode 100644 index 00000000..5a946e16 --- /dev/null +++ b/Tools/SeqLock/Program.cs @@ -0,0 +1,29 @@ +using SeqLock; + +Console.WriteLine("Torn write test"); + +while (true) +{ + Console.WriteLine("Enter"); + Console.WriteLine("1 for repro"); + Console.WriteLine("2 for SeqLock (no repro)"); + string? s = Console.ReadLine(); + + if (int.TryParse(s, out var i)) + { + switch (i) + { + case 1: + // repros + new TornProgram().run(); + break; + case 2: + // does not repro + new TornProgramSeqLock().run(); + break; + default: + Console.WriteLine("Input must be either 1 or 2"); + break; + } + } +} diff --git a/Tools/SeqLock/SeqLock.csproj b/Tools/SeqLock/SeqLock.csproj new file mode 100644 index 00000000..38cb7be6 --- /dev/null +++ b/Tools/SeqLock/SeqLock.csproj @@ -0,0 +1,11 @@ + + + + Exe + net6.0 + enable + enable + x86 + + + diff --git a/Tools/SeqLock/TornProgram.cs b/Tools/SeqLock/TornProgram.cs new file mode 100644 index 00000000..2cf15f8f --- /dev/null +++ b/Tools/SeqLock/TornProgram.cs @@ -0,0 +1,40 @@ +namespace SeqLock +{ + // https://stackoverflow.com/questions/23262513/reproduce-torn-reads-of-decimal-in-c-sharp + internal class TornProgram + { + public void run() + { + Task.Run((Action) setter); + Task.Run((Action) checker); + + Console.WriteLine("Press to stop"); + Console.ReadLine(); + } + + void setter() + { + while (true) + { + d = VALUE1; + d = VALUE2; + } + } + + void checker() + { + for (int count = 0;; ++count) + { + var t = d; + + if (t != VALUE1 && t != VALUE2) + Console.WriteLine("Value is torn after {0} iterations: {1}", count, t); + } + } + + Decimal d; + + const Decimal VALUE1 = 1m; + const Decimal VALUE2 = 10000000000m; + } +} diff --git a/Tools/SeqLock/TornProgramSeqLock.cs b/Tools/SeqLock/TornProgramSeqLock.cs new file mode 100644 index 00000000..e0ee4257 --- /dev/null +++ b/Tools/SeqLock/TornProgramSeqLock.cs @@ -0,0 +1,39 @@ +namespace SeqLock +{ + internal class TornProgramSeqLock + { + public void run() + { + Task.Run((Action) setter); + Task.Run((Action) checker); + + Console.WriteLine("Press to stop"); + Console.ReadLine(); + } + + void setter() + { + while (true) + { + d1.Write(VALUE1); + d1.Write(VALUE2); + } + } + + void checker() + { + for (int count = 0;; ++count) + { + var t = d1.Read(); + + if (t != VALUE1 && t != VALUE2) + Console.WriteLine("SeqLockValue is torn after {0} iterations: {1}", count, t); + } + } + + LruItem d1 = new LruItem(1, VALUE1); + + const Decimal VALUE1 = 1m; + const Decimal VALUE2 = 10000000000m; + } +} diff --git a/Tools/Tools.sln b/Tools/Tools.sln index edb34c8d..2e26f24a 100644 --- a/Tools/Tools.sln +++ b/Tools/Tools.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HashTableSize", "HashTableS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchPlot", "BenchPlot\BenchPlot.csproj", "{A3AA1ECB-E753-4E73-B2A8-09434FAF2664}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeqLock", "SeqLock\SeqLock.csproj", "{27E4861A-EB60-4D7B-B436-DC71EE89F32A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {A3AA1ECB-E753-4E73-B2A8-09434FAF2664}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3AA1ECB-E753-4E73-B2A8-09434FAF2664}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3AA1ECB-E753-4E73-B2A8-09434FAF2664}.Release|Any CPU.Build.0 = Release|Any CPU + {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 126a16ae22ffe8130b13c9185a8ed1cdd87d7161 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 20 May 2024 14:51:07 -0700 Subject: [PATCH 06/13] soak test --- .../Lru/ConcurrentLruSoakTests.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs index d78262c9..98762152 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BitFaster.Caching.Lru; @@ -190,6 +191,29 @@ await Threaded.Run(4, () => { } } + [Fact] + public async Task WhenSoakConcurrentGetAndUpdateValueTypeCacheEndsInConsistentState() + { + var lruVT = new ConcurrentLru(1, capacity, EqualityComparer.Default); + + for (int i = 0; i < 10; i++) + { + await Threaded.Run(4, () => { + var b = new byte[8]; + for (int i = 0; i < 100000; i++) + { + lruVT.TryUpdate(i + 1, new Guid(i, 0, 0, b)); + lruVT.GetOrAdd(i + 1, x => new Guid(x, 0, 0, b)); + } + }); + + this.testOutputHelper.WriteLine($"{lruVT.HotCount} {lruVT.WarmCount} {lruVT.ColdCount}"); + this.testOutputHelper.WriteLine(string.Join(" ", lruVT.Keys)); + + new ConcurrentLruIntegrityChecker, LruPolicy, TelemetryPolicy>(lruVT).Validate(); + } + } + [Fact] public async Task WhenAddingCacheSizeItemsNothingIsEvicted() { From 8ff919523cbc7051c08b7f5fae1df81ecf8e5730 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 20 May 2024 19:48:43 -0700 Subject: [PATCH 07/13] upd cmts --- BitFaster.Caching/Lru/LruItem.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BitFaster.Caching/Lru/LruItem.cs b/BitFaster.Caching/Lru/LruItem.cs index d4f40c4a..fd120414 100644 --- a/BitFaster.Caching/Lru/LruItem.cs +++ b/BitFaster.Caching/Lru/LruItem.cs @@ -13,6 +13,7 @@ public class LruItem private volatile bool wasAccessed; private volatile bool wasRemoved; + // only used when V is a non-atomic value type to prevent torn reads private int sequence; /// @@ -63,7 +64,7 @@ internal V SeqLockRead() if ((start & 1) == 1) { - // A write is in progress. Back off and keep spinning. + // A write is in progress, spin. spin.SpinOnce(); continue; } @@ -78,7 +79,7 @@ internal V SeqLockRead() } } - // Note: item should be locked while invoking this method. Multiple writer threads are not supported. + // Note: LruItem should be locked while invoking this method. Multiple writer threads are not supported. internal void SeqLockWrite(V value) { Interlocked.Increment(ref sequence); From 84155675ccccfdcf20e1f890587b43ab71068267 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 21 May 2024 11:49:45 -0700 Subject: [PATCH 08/13] matching repro --- Tools/SeqLock/TornProgram.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tools/SeqLock/TornProgram.cs b/Tools/SeqLock/TornProgram.cs index 2cf15f8f..bcb3dbc4 100644 --- a/Tools/SeqLock/TornProgram.cs +++ b/Tools/SeqLock/TornProgram.cs @@ -16,8 +16,8 @@ void setter() { while (true) { - d = VALUE1; - d = VALUE2; + d1.Value = VALUE1; + d1.Value = VALUE2; } } @@ -25,14 +25,14 @@ void checker() { for (int count = 0;; ++count) { - var t = d; + var t = d1.Value; if (t != VALUE1 && t != VALUE2) Console.WriteLine("Value is torn after {0} iterations: {1}", count, t); } } - Decimal d; + LruItem d1 = new LruItem(1, VALUE1); const Decimal VALUE1 = 1m; const Decimal VALUE2 = 10000000000m; From 86cb7e2700ac0cc31d095af661151632f0865cdc Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 21 May 2024 19:53:33 -0700 Subject: [PATCH 09/13] soak test --- .../Lru/LruItemSoakTests.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs diff --git a/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs b/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs new file mode 100644 index 00000000..832c5f70 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs @@ -0,0 +1,118 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + [Collection("Soak")] + public class LruItemSoakTests + { + private const int soakIterations = 3; + private readonly LruItem item = new(1, MassiveStruct.A); + + // Adapted from + // https://stackoverflow.com/questions/23262513/reproduce-torn-reads-of-decimal-in-c-sharp + [Theory] + [Repeat(soakIterations)] + #pragma warning disable xUnit1026 + public async Task DetectTornStruct(int iteration) + #pragma warning restore xUnit1026 + { + using var source = new CancellationTokenSource(); + var started = new TaskCompletionSource(); + + var setTask = Task.Run(() => Setter(source.Token, started)); + await started.Task; + Checker(source); + + await setTask; + } + + void Setter(CancellationToken cancelToken, TaskCompletionSource started) + { + started.SetResult(true); + + while (true) + { + item.SeqLockWrite(MassiveStruct.A); + item.SeqLockWrite(MassiveStruct.B); + + if (cancelToken.IsCancellationRequested) + { + return; + } + } + } + + void Checker(CancellationTokenSource source) + { + // On my machine, without SeqLock, this consistently fails below 100 iterations + // on debug build, and below 1000 on release build + for (int count = 0; count < 10_000; ++count) + { + var t = item.SeqLockRead(); + + if (t != MassiveStruct.A && t != MassiveStruct.B) + { + throw new Exception($"Value is torn after {count} iterations"); + } + } + + source.Cancel(); + } + + #pragma warning disable CS0659 // Object.Equals but no GetHashCode + #pragma warning disable CS0661 // operator== but no GetHashCode + public struct MassiveStruct : IEquatable + { + // To repro on x64, struct should be larger than a cache line (64 bytes). + public long a; + public long b; + public long c; + public long d; + + public long e; + public long f; + public long g; + public long h; + + public long i; + + public static readonly MassiveStruct A = new MassiveStruct(); + public static readonly MassiveStruct B = new MassiveStruct() + { a = long.MaxValue, b = long.MaxValue, c = long.MaxValue, d = long.MaxValue, e = long.MaxValue, f= long.MaxValue, g = long.MaxValue, h = long.MaxValue, i = long.MaxValue }; + + public override bool Equals(object obj) + { + return obj is MassiveStruct @struct && Equals(@struct); + } + + public bool Equals(MassiveStruct other) + { + return a == other.a && + b == other.b && + c == other.c && + d == other.d && + e == other.e && + f == other.f && + g == other.g && + h == other.h && + i == other.i; + } + + public static bool operator ==(MassiveStruct left, MassiveStruct right) + { + return left.Equals(right); + } + + public static bool operator !=(MassiveStruct left, MassiveStruct right) + { + return !(left == right); + } + } + #pragma warning restore CS0659 + #pragma warning restore CS0661 + } +} From 11425980e0929554f7e1ec5741adf4fd851fa626 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 23 May 2024 14:54:00 -0700 Subject: [PATCH 10/13] rem repro --- Tools/SeqLock/LruItem.cs | 87 ----------------------------- Tools/SeqLock/Program.cs | 29 ---------- Tools/SeqLock/SeqLock.csproj | 11 ---- Tools/SeqLock/TornProgram.cs | 40 ------------- Tools/SeqLock/TornProgramSeqLock.cs | 39 ------------- Tools/Tools.sln | 6 -- 6 files changed, 212 deletions(-) delete mode 100644 Tools/SeqLock/LruItem.cs delete mode 100644 Tools/SeqLock/Program.cs delete mode 100644 Tools/SeqLock/SeqLock.csproj delete mode 100644 Tools/SeqLock/TornProgram.cs delete mode 100644 Tools/SeqLock/TornProgramSeqLock.cs diff --git a/Tools/SeqLock/LruItem.cs b/Tools/SeqLock/LruItem.cs deleted file mode 100644 index 7a8a5133..00000000 --- a/Tools/SeqLock/LruItem.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace SeqLock -{ - public class LruItem - { - private volatile bool wasAccessed; - private volatile bool wasRemoved; - - private int sequence; - - /// - /// Initializes a new instance of the LruItem class with the specified key and value. - /// - /// The key. - /// The value. - public LruItem(K k, V v) - { - this.Key = k; - this.Value = v; - } - - /// - /// Gets the key. - /// - public readonly K Key; - - /// - /// Gets or sets the value. - /// - public V Value { get; set; } - - /// - /// Gets or sets a value indicating whether the item was accessed. - /// - public bool WasAccessed - { - get => this.wasAccessed; - set => this.wasAccessed = value; - } - - /// - /// Gets or sets a value indicating whether the item was removed. - /// - public bool WasRemoved - { - get => this.wasRemoved; - set => this.wasRemoved = value; - } - - public V Read() - { - //return this.Value; - - var spin = new SpinWait(); - while (true) - { - var start = Volatile.Read(ref this.sequence); - - if ((start & 1) == 1) - { - // A write is in progress. Back off and keep spinning. - spin.SpinOnce(); - continue; - } - - V copy = this.Value; - - var end = Volatile.Read(ref this.sequence); - if (start == end) - { - return copy; - } - } - } - - public void Write(V value) - { - lock (this) - { - Interlocked.Increment(ref sequence); - - this.Value = value; - - Interlocked.Increment(ref sequence); - } - } - } -} diff --git a/Tools/SeqLock/Program.cs b/Tools/SeqLock/Program.cs deleted file mode 100644 index 5a946e16..00000000 --- a/Tools/SeqLock/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -using SeqLock; - -Console.WriteLine("Torn write test"); - -while (true) -{ - Console.WriteLine("Enter"); - Console.WriteLine("1 for repro"); - Console.WriteLine("2 for SeqLock (no repro)"); - string? s = Console.ReadLine(); - - if (int.TryParse(s, out var i)) - { - switch (i) - { - case 1: - // repros - new TornProgram().run(); - break; - case 2: - // does not repro - new TornProgramSeqLock().run(); - break; - default: - Console.WriteLine("Input must be either 1 or 2"); - break; - } - } -} diff --git a/Tools/SeqLock/SeqLock.csproj b/Tools/SeqLock/SeqLock.csproj deleted file mode 100644 index 38cb7be6..00000000 --- a/Tools/SeqLock/SeqLock.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Exe - net6.0 - enable - enable - x86 - - - diff --git a/Tools/SeqLock/TornProgram.cs b/Tools/SeqLock/TornProgram.cs deleted file mode 100644 index bcb3dbc4..00000000 --- a/Tools/SeqLock/TornProgram.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace SeqLock -{ - // https://stackoverflow.com/questions/23262513/reproduce-torn-reads-of-decimal-in-c-sharp - internal class TornProgram - { - public void run() - { - Task.Run((Action) setter); - Task.Run((Action) checker); - - Console.WriteLine("Press to stop"); - Console.ReadLine(); - } - - void setter() - { - while (true) - { - d1.Value = VALUE1; - d1.Value = VALUE2; - } - } - - void checker() - { - for (int count = 0;; ++count) - { - var t = d1.Value; - - if (t != VALUE1 && t != VALUE2) - Console.WriteLine("Value is torn after {0} iterations: {1}", count, t); - } - } - - LruItem d1 = new LruItem(1, VALUE1); - - const Decimal VALUE1 = 1m; - const Decimal VALUE2 = 10000000000m; - } -} diff --git a/Tools/SeqLock/TornProgramSeqLock.cs b/Tools/SeqLock/TornProgramSeqLock.cs deleted file mode 100644 index e0ee4257..00000000 --- a/Tools/SeqLock/TornProgramSeqLock.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace SeqLock -{ - internal class TornProgramSeqLock - { - public void run() - { - Task.Run((Action) setter); - Task.Run((Action) checker); - - Console.WriteLine("Press to stop"); - Console.ReadLine(); - } - - void setter() - { - while (true) - { - d1.Write(VALUE1); - d1.Write(VALUE2); - } - } - - void checker() - { - for (int count = 0;; ++count) - { - var t = d1.Read(); - - if (t != VALUE1 && t != VALUE2) - Console.WriteLine("SeqLockValue is torn after {0} iterations: {1}", count, t); - } - } - - LruItem d1 = new LruItem(1, VALUE1); - - const Decimal VALUE1 = 1m; - const Decimal VALUE2 = 10000000000m; - } -} diff --git a/Tools/Tools.sln b/Tools/Tools.sln index 2e26f24a..edb34c8d 100644 --- a/Tools/Tools.sln +++ b/Tools/Tools.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HashTableSize", "HashTableS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchPlot", "BenchPlot\BenchPlot.csproj", "{A3AA1ECB-E753-4E73-B2A8-09434FAF2664}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeqLock", "SeqLock\SeqLock.csproj", "{27E4861A-EB60-4D7B-B436-DC71EE89F32A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,10 +21,6 @@ Global {A3AA1ECB-E753-4E73-B2A8-09434FAF2664}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3AA1ECB-E753-4E73-B2A8-09434FAF2664}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3AA1ECB-E753-4E73-B2A8-09434FAF2664}.Release|Any CPU.Build.0 = Release|Any CPU - {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27E4861A-EB60-4D7B-B436-DC71EE89F32A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From e465a4db2d1bc952f994bec77b26633d1d2a9c07 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 23 May 2024 19:37:09 -0700 Subject: [PATCH 11/13] tests --- .../Lru/ConcurrentLruSoakTests.cs | 48 +++++++++++++++++++ .../Lru/LruItemSoakTests.cs | 8 ++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs index 98762152..fc473381 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using BitFaster.Caching.Lru; using FluentAssertions; using Xunit; using Xunit.Abstractions; +using static BitFaster.Caching.UnitTests.Lru.LruItemSoakTests; namespace BitFaster.Caching.UnitTests.Lru { @@ -282,6 +284,52 @@ await Threaded.Run(4, r => { RunIntegrityCheck(); } + // This test will run forever if there is a live lock. + // Since the cache bookkeeping has some overhead, it is harder to provoke + // spinning inside the reader thread compared to LruItemSoakTests.DetectTornStruct. + [Theory] + [Repeat(10)] + public async Task WhenValueIsBigStructNoLiveLock(int _) + { + using var source = new CancellationTokenSource(); + var started = new TaskCompletionSource(); + var cache = new ConcurrentLru(1, capacity, EqualityComparer.Default); + + var setTask = Task.Run(() => Setter(cache, source.Token, started)); + await started.Task; + Checker(cache, source); + + await setTask; + } + + private void Setter(ICache cache, CancellationToken cancelToken, TaskCompletionSource started) + { + started.SetResult(true); + + while (true) + { + cache.AddOrUpdate(1, Guid.NewGuid()); + cache.AddOrUpdate(1, Guid.NewGuid()); + + if (cancelToken.IsCancellationRequested) + { + return; + } + } + } + + private void Checker(ICache cache,CancellationTokenSource source) + { + // On my machine, without SeqLock, this consistently fails below 100 iterations + // on debug build, and below 1000 on release build + for (int count = 0; count < 100_000; ++count) + { + cache.TryGet(1, out _); + } + + source.Cancel(); + } + private void RunIntegrityCheck() { new ConcurrentLruIntegrityChecker, LruPolicy, TelemetryPolicy>(this.lru).Validate(); diff --git a/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs b/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs index 832c5f70..4bc949a6 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruItemSoakTests.cs @@ -16,9 +16,7 @@ public class LruItemSoakTests // https://stackoverflow.com/questions/23262513/reproduce-torn-reads-of-decimal-in-c-sharp [Theory] [Repeat(soakIterations)] - #pragma warning disable xUnit1026 - public async Task DetectTornStruct(int iteration) - #pragma warning restore xUnit1026 + public async Task DetectTornStruct(int _) { using var source = new CancellationTokenSource(); var started = new TaskCompletionSource(); @@ -30,7 +28,7 @@ public async Task DetectTornStruct(int iteration) await setTask; } - void Setter(CancellationToken cancelToken, TaskCompletionSource started) + private void Setter(CancellationToken cancelToken, TaskCompletionSource started) { started.SetResult(true); @@ -46,7 +44,7 @@ void Setter(CancellationToken cancelToken, TaskCompletionSource started) } } - void Checker(CancellationTokenSource source) + private void Checker(CancellationTokenSource source) { // On my machine, without SeqLock, this consistently fails below 100 iterations // on debug build, and below 1000 on release build From 0aa999d852a3eeda1f915df111235b933137c467 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 25 May 2024 16:26:50 -0700 Subject: [PATCH 12/13] encapsulate --- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 21 ++---------- BitFaster.Caching/Lru/LruItem.cs | 37 +++++++++++++++++++--- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 1cc04f8a..adbca475 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -179,15 +179,7 @@ private bool GetOrDiscard(I item, [MaybeNullWhen(false)] out V value) return false; } - if (TypeProps.IsWriteAtomic) - { - value = item.Value; - } - else - { - // prevent torn read for non-atomic types - value = item.SeqLockRead(); - } + value = item.Value; this.itemPolicy.Touch(item); this.telemetryPolicy.IncrementHit(); @@ -392,15 +384,8 @@ public bool TryUpdate(K key, V value) { V oldValue = existing.Value; - if (TypeProps.IsWriteAtomic) - { - existing.Value = value; - } - else - { - existing.SeqLockWrite(value); - } - + existing.Value = value; + this.itemPolicy.Update(existing); // backcompat: remove conditional compile #if NETCOREAPP3_0_OR_GREATER diff --git a/BitFaster.Caching/Lru/LruItem.cs b/BitFaster.Caching/Lru/LruItem.cs index 384351e7..f2e865c9 100644 --- a/BitFaster.Caching/Lru/LruItem.cs +++ b/BitFaster.Caching/Lru/LruItem.cs @@ -1,4 +1,5 @@  +using System.Runtime.CompilerServices; using System.Threading; namespace BitFaster.Caching.Lru @@ -11,6 +12,8 @@ namespace BitFaster.Caching.Lru public class LruItem where K : notnull { + private V data; + private volatile bool wasAccessed; private volatile bool wasRemoved; @@ -25,7 +28,7 @@ public class LruItem public LruItem(K k, V v) { this.Key = k; - this.Value = v; + this.data = v; } /// @@ -36,7 +39,33 @@ public LruItem(K k, V v) /// /// Gets or sets the value. /// - public V Value { get; set; } + public V Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (TypeProps.IsWriteAtomic) + { + return data; + } + else + { + return SeqLockRead(); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + if (TypeProps.IsWriteAtomic) + { + data = value; + } + else + { + SeqLockWrite(value); + } + } + } /// /// Gets or sets a value indicating whether the item was accessed. @@ -70,7 +99,7 @@ internal V SeqLockRead() continue; } - V copy = this.Value; + V copy = this.data; var end = Volatile.Read(ref this.sequence); if (start == end) @@ -85,7 +114,7 @@ internal void SeqLockWrite(V value) { Interlocked.Increment(ref sequence); - this.Value = value; + this.data = value; Interlocked.Increment(ref sequence); } From 7a88edf95aa0f49b88c88d138ce816517ec89c5c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 25 May 2024 18:09:29 -0700 Subject: [PATCH 13/13] correct obj dump --- .../Lru/LruItemMemoryLayoutDumps.cs | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs b/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs index a46b5f30..a2ea5a43 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruItemMemoryLayoutDumps.cs @@ -1,4 +1,5 @@ -using BitFaster.Caching.Lru; +using System; +using BitFaster.Caching.Lru; using ObjectLayoutInspector; using Xunit; using Xunit.Abstractions; @@ -16,23 +17,23 @@ public LruItemMemoryLayoutDumps(ITestOutputHelper testOutputHelper) //Type layout for 'LruItem`2' //Size: 24 bytes. Paddings: 2 bytes (%8 of empty space) - //|================================================| - //| Object Header (8 bytes) | - //|------------------------------------------------| - //| Method Table Ptr (8 bytes) | - //|================================================| - //| 0-7: Object Key (8 bytes) | - //|------------------------------------------------| - //| 8-15: Object k__BackingField (8 bytes) | - //|------------------------------------------------| - //| 16-19: Int32 sequence (4 bytes) | - //|------------------------------------------------| - //| 20: Boolean wasAccessed (1 byte) | - //|------------------------------------------------| - //| 21: Boolean wasRemoved (1 byte) | - //|------------------------------------------------| - //| 22-23: padding (2 bytes) | - //|================================================| + //|=====================================| + //| Object Header (8 bytes) | + //|-------------------------------------| + //| Method Table Ptr (8 bytes) | + //|=====================================| + //| 0-7: Object data (8 bytes) | + //|-------------------------------------| + //| 8-15: Object Key (8 bytes) | + //|-------------------------------------| + //| 16-19: Int32 sequence (4 bytes) | + //|-------------------------------------| + //| 20: Boolean wasAccessed (1 byte) | + //|-------------------------------------| + //| 21: Boolean wasRemoved (1 byte) | + //|-------------------------------------| + //| 22-23: padding (2 bytes) | + //|=====================================| [Fact] public void DumpLruItem() { @@ -47,9 +48,9 @@ public void DumpLruItem() //|---------------------------------------------------| //| Method Table Ptr (8 bytes) | //|===================================================| - //| 0-7: Object Key (8 bytes) | + //| 0-7: Object data (8 bytes) | //|---------------------------------------------------| - //| 8-15: Object k__BackingField (8 bytes) | + //| 8-15: Object Key (8 bytes) | //|---------------------------------------------------| //| 16-19: Int32 sequence (4 bytes) | //|---------------------------------------------------|