From 4fca89279cf320a63c697b9efe80ebb9591a0f28 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 25 Aug 2026 20:52:15 -0700 Subject: [PATCH 1/8] counter --- BitFaster.Caching/Counters/Counter.cs | 58 ++++++++++++++++++++++--- BitFaster.Caching/Counters/Striped64.cs | 50 +++++++++++++-------- 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/BitFaster.Caching/Counters/Counter.cs b/BitFaster.Caching/Counters/Counter.cs index fb2f9634..630b9ee5 100644 --- a/BitFaster.Caching/Counters/Counter.cs +++ b/BitFaster.Caching/Counters/Counter.cs @@ -1,15 +1,58 @@ -/* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at - * http://creativecommons.org/publicdomain/zero/1.0/ - */ +#if NET9_0_OR_GREATER +using System; +using System.Threading; +#endif namespace BitFaster.Caching.Counters { /// /// A thread-safe counter suitable for high throuhgput counting across many concurrent threads. /// - /// Based on the LongAdder class by Doug Lea. +#if NET9_0_OR_GREATER + + public sealed class Counter : Striped64 + { + private PaddedLong[] Deltas = new PaddedLong[Environment.ProcessorCount]; + + /// + /// Increment by 1. + /// + public void Increment() + { + Add(1L); + } + + /// + /// Adds the specified value. + /// + /// The value to add. + public void Add(long value) + { + ref PaddedLong delta = ref Deltas[(uint)Thread.GetCurrentProcessorId() % (uint)Deltas.Length]; + Interlocked.Add(ref delta.value, value); + } + + /// + /// Computes the current count. + /// + /// The current count. + public long Count() + { + long delta = 0; + foreach (ref PaddedLong i in Deltas.AsSpan()) + { + delta += Interlocked.Exchange(ref i.value, 0); + } + return delta; + } + } + +#else + /* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ public sealed class Counter : Striped64 { /// @@ -20,7 +63,7 @@ public Counter() { } /// /// Computes the current count. /// - /// The current sum. + /// The current count. public long Count() { var @as = this.Cells; Cell a; @@ -64,4 +107,5 @@ public void Add(long value) } } } +#endif } diff --git a/BitFaster.Caching/Counters/Striped64.cs b/BitFaster.Caching/Counters/Striped64.cs index 370f6cd7..1c4fc93c 100644 --- a/BitFaster.Caching/Counters/Striped64.cs +++ b/BitFaster.Caching/Counters/Striped64.cs @@ -80,21 +80,12 @@ namespace BitFaster.Caching.Counters [ExcludeFromCodeCoverage] public abstract class Striped64 { - // Number of CPUS, to place bound on table size - private static readonly int MaxBuckets = Environment.ProcessorCount * 4; - /// /// The base value used mainly when there is no contention, but also as a fallback /// during table initialization races. Updated via CAS. /// protected PaddedLong @base = new(); - /// - /// When non-null, size is a power of 2. - /// - protected Cell[]? Cells; - private int cellsBusy; - /// /// A wrapper for PaddedLong. /// @@ -115,27 +106,47 @@ public Cell(long x) } } + /// + /// When non-null, size is a power of 2. + /// + protected Cell[]? Cells; + /** - * CASes the cellsBusy field from 0 to 1 to acquire lock. + * Returns the probe value for the current thread. + * Duplicated from ThreadLocalRandom because of packaging restrictions. */ - private bool CasCellsBusy() + protected static int GetProbe() { - return Interlocked.CompareExchange(ref this.cellsBusy, 1, 0) == 0; + // Note: this results in higher throughput than introducing a random. + return Environment.CurrentManagedThreadId; } - private void VolatileWriteNotBusy() +#if NET9_0_OR_GREATER +#pragma warning disable CA1822 // Mark members as static + /// + /// Not used on .NET 9.0 + /// + protected void LongAccumulate(long x, bool wasUncontended) { - Volatile.Write(ref this.cellsBusy, 0); } +#pragma warning restore CA1822 // Mark members as static +#else + // Number of CPUS, to place bound on table size + private static readonly int MaxBuckets = Environment.ProcessorCount * 4; + + private int cellsBusy; /** - * Returns the probe value for the current thread. - * Duplicated from ThreadLocalRandom because of packaging restrictions. + * CASes the cellsBusy field from 0 to 1 to acquire lock. */ - protected static int GetProbe() + private bool CasCellsBusy() { - // Note: this results in higher throughput than introducing a random. - return Environment.CurrentManagedThreadId; + return Interlocked.CompareExchange(ref this.cellsBusy, 1, 0) == 0; + } + + private void VolatileWriteNotBusy() + { + Volatile.Write(ref this.cellsBusy, 0); } /** @@ -250,5 +261,6 @@ protected void LongAccumulate(long x, bool wasUncontended) break; } } +#endif } } From 6d3c1209a637906b2fc77cf68e107e8f1882f0e2 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 25 Aug 2026 21:24:22 -0700 Subject: [PATCH 2/8] bench --- .../CounterBenchmark.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 BitFaster.Caching.Benchmarks/CounterBenchmark.cs diff --git a/BitFaster.Caching.Benchmarks/CounterBenchmark.cs b/BitFaster.Caching.Benchmarks/CounterBenchmark.cs new file mode 100644 index 00000000..a8f9dd6f --- /dev/null +++ b/BitFaster.Caching.Benchmarks/CounterBenchmark.cs @@ -0,0 +1,111 @@ + +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Diagnostics.Tracing; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace BitFaster.Caching.Benchmarks +{ +#if Windows + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] +#endif + [HideColumns("Job", "Median", "RatioSD", "Alloc Ratio")] + public class CounterBenchmark + { + const int Iters = 1_000_000; + + private Counters.Counter counter = new Counters.Counter(); + + private Meter meter; + private Counter metricsCounter; + private UpDownCounter upDownCounter; + private MetricsEventListener listener; + + [GlobalSetup] + public void Setup() + { + meter = new Meter("Example"); + upDownCounter = meter.CreateUpDownCounter("upDownCounter"); + metricsCounter = meter.CreateCounter("counter"); + listener = new MetricsEventListener(); + } + + [GlobalCleanup] + public void Cleanup() + { + meter.Dispose(); + } + + [Benchmark] + public void CounterSerial() + { + for (int i = 0; i < Iters; i++) + { + counter.Add(1); + counter.Add(1); + } + } + + [Benchmark] + public void CounterParallel() + { + Parallel.For(0, Iters, i => + { + counter.Add(1); + counter.Add(1); + }); + } + + [Benchmark] + public void MetricsCounterSerial() + { + for (int i = 0; i < Iters; i++) + { + metricsCounter.Add(1); + metricsCounter.Add(1); + } + } + + [Benchmark] + public void MetricsCounterParallel() + { + Parallel.For(0, Iters, i => + { + metricsCounter.Add(1); + metricsCounter.Add(1); + }); + } + + [Benchmark] + public void UpDownCounterSerial() + { + for (int i = 0; i < Iters; i++) + { + upDownCounter.Add(1); + upDownCounter.Add(-1); + } + } + + [Benchmark] + public void UpDownCounterParallel() + { + Parallel.For(0, Iters, i => + { + upDownCounter.Add(1); + upDownCounter.Add(-1); + }); + } + + private sealed class MetricsEventListener : EventListener + { + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name == "System.Diagnostics.Metrics") + { + EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All, new Dictionary() { { "Metrics", "Example\\upDownCounter;Example\\counter" } }); + } + } + } + } +} From e509292f711c2a4ebc098eed605b878566a6d5f0 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 25 Aug 2026 21:53:18 -0700 Subject: [PATCH 3/8] compare to existing --- .../CounterBenchmark.cs | 24 ++ .../Striped64Counter.cs | 304 ++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 BitFaster.Caching.Benchmarks/Striped64Counter.cs diff --git a/BitFaster.Caching.Benchmarks/CounterBenchmark.cs b/BitFaster.Caching.Benchmarks/CounterBenchmark.cs index a8f9dd6f..5439cfdc 100644 --- a/BitFaster.Caching.Benchmarks/CounterBenchmark.cs +++ b/BitFaster.Caching.Benchmarks/CounterBenchmark.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Metrics; using System.Diagnostics.Tracing; using System.Threading.Tasks; +using Benchly; using BenchmarkDotNet.Attributes; namespace BitFaster.Caching.Benchmarks @@ -11,12 +12,15 @@ namespace BitFaster.Caching.Benchmarks [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] #endif [HideColumns("Job", "Median", "RatioSD", "Alloc Ratio")] + [ColumnChart(Title = "Counter Latency ({JOB})", Output = OutputMode.PerJob, Colors = "seagreen,darkgreen,thistle,plum,lightcoral,indianred,lightpink,hotpink")] public class CounterBenchmark { const int Iters = 1_000_000; private Counters.Counter counter = new Counters.Counter(); + private Striped64Counter striped64Counter = new Striped64Counter(); + private Meter meter; private Counter metricsCounter; private UpDownCounter upDownCounter; @@ -57,6 +61,26 @@ public void CounterParallel() }); } + [Benchmark] + public void Striped64CounterSerial() + { + for (int i = 0; i < Iters; i++) + { + striped64Counter.Add(1); + striped64Counter.Add(1); + } + } + + [Benchmark] + public void Striped64CounterParallel() + { + Parallel.For(0, Iters, i => + { + striped64Counter.Add(1); + striped64Counter.Add(1); + }); + } + [Benchmark] public void MetricsCounterSerial() { diff --git a/BitFaster.Caching.Benchmarks/Striped64Counter.cs b/BitFaster.Caching.Benchmarks/Striped64Counter.cs new file mode 100644 index 00000000..df654a69 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/Striped64Counter.cs @@ -0,0 +1,304 @@ +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +using System; +using System.Threading; +using BitFaster.Caching.Counters; + +namespace BitFaster.Caching.Benchmarks +{ + /* + * This class maintains a lazily-initialized table of atomically + * updated variables, plus an extra "base" field. The table size + * is a power of two. Indexing uses masked per-thread hash codes. + * Nearly all declarations in this class are package-private, + * accessed directly by subclasses. + * + * Table entries are of class Cell; a variant of AtomicLong padded + * to reduce cache contention on most processors. Padding is + * overkill for most Atomics because they are usually irregularly + * scattered in memory and thus don't interfere much with each + * other. But Atomic objects residing in arrays will tend to be + * placed adjacent to each other, and so will most often share + * cache lines (with a huge negative performance impact) without + * this precaution. + * + * In part because Cells are relatively large, we avoid creating + * them until they are needed. When there is no contention, all + * updates are made to the base field. Upon first contention (a + * failed CAS on base update), the table is initialized to size 2. + * The table size is doubled upon further contention until + * reaching the nearest power of two greater than or equal to the + * number of CPUS. Table slots remain empty (null) until they are + * needed. + * + * A single spinlock ("busy") is used for initializing and + * resizing the table, as well as populating slots with new Cells. + * There is no need for a blocking lock; when the lock is not + * available, threads try other slots (or the base). During these + * retries, there is increased contention and reduced locality, + * which is still better than alternatives. + * + * Per-thread hash codes are initialized to random values. + * Contention and/or table collisions are indicated by failed + * CASes when performing an update operation (see method + * retryUpdate). Upon a collision, if the table size is less than + * the capacity, it is doubled in size unless some other thread + * holds the lock. If a hashed slot is empty, and lock is + * available, a new Cell is created. Otherwise, if the slot + * exists, a CAS is tried. Retries proceed by "double hashing", + * using a secondary hash (Marsaglia XorShift) to try to find a + * free slot. + * + * The table size is capped because, when there are more threads + * than CPUs, supposing that each thread were bound to a CPU, + * there would exist a perfect hash function mapping threads to + * slots that eliminates collisions. When we reach capacity, we + * search for this mapping by randomly varying the hash codes of + * colliding threads. Because search is random, and collisions + * only become known via CAS failures, convergence can be slow, + * and because threads are typically not bound to CPUS forever, + * may not occur at all. However, despite these limitations, + * observed contention rates are typically low in these cases. + * + * It is possible for a Cell to become unused when threads that + * once hashed to it terminate, as well as in the case where + * doubling the table causes no thread to hash to it under + * expanded mask. We do not try to detect or remove such cells, + * under the assumption that for long-running instances, observed + * contention levels will recur, so the cells will eventually be + * needed again; and for short-lived ones, it does not matter. + */ + public class Striped64Counter : Striped64Internal + { + /// + /// Creates a new Counter with an intial sum of zero. + /// + public Striped64Counter() { } + + /// + /// Computes the current count. + /// + /// The current count. + public long Count() + { + var @as = this.Cells; Cell a; + var sum = @base.VolatileRead(); + if (@as != null) + { + for (var i = 0; i < @as.Length; ++i) + { + if ((a = @as[i]) != null) + sum += a.value.VolatileRead(); + } + } + return sum; + } + + /// + /// Increment by 1. + /// + public void Increment() + { + Add(1L); + } + + /// + /// Adds the specified value. + /// + /// The value to add. + public void Add(long value) + { + Cell[]? @as; + long b, v; + int m; + Cell a; + if ((@as = this.Cells) != null || !@base.CompareAndSwap(b = @base.VolatileRead(), b + value)) + { + var uncontended = true; + if (@as == null || (m = @as.Length - 1) < 0 || (a = @as[GetProbe() & m]) == null || !(uncontended = a.value.CompareAndSwap(v = a.value.VolatileRead(), v + value))) + { + LongAccumulate(value, uncontended); + } + } + } + } + + public abstract class Striped64Internal + { + /// + /// The base value used mainly when there is no contention, but also as a fallback + /// during table initialization races. Updated via CAS. + /// + protected PaddedLong @base = new(); + + /// + /// A wrapper for PaddedLong. + /// + protected sealed class Cell + { + /// + /// The value of the cell. + /// + public PaddedLong value; + + /// + /// Initializes a new cell with the specified value. + /// + /// The value. + public Cell(long x) + { + this.value = new PaddedLong() { value = x }; + } + } + + /// + /// When non-null, size is a power of 2. + /// + protected Cell[]? Cells; + + /** + * Returns the probe value for the current thread. + * Duplicated from ThreadLocalRandom because of packaging restrictions. + */ + protected static int GetProbe() + { + // Note: this results in higher throughput than introducing a random. + return Environment.CurrentManagedThreadId; + } + + // Number of CPUS, to place bound on table size + private static readonly int MaxBuckets = Environment.ProcessorCount * 4; + + private int cellsBusy; + + /** + * CASes the cellsBusy field from 0 to 1 to acquire lock. + */ + private bool CasCellsBusy() + { + return Interlocked.CompareExchange(ref this.cellsBusy, 1, 0) == 0; + } + + private void VolatileWriteNotBusy() + { + Volatile.Write(ref this.cellsBusy, 0); + } + + /** + * Pseudo-randomly advances and records the given probe value for the + * given thread. + * Duplicated from ThreadLocalRandom because of packaging restrictions. + */ + private static int AdvanceProbe(int probe) + { + probe ^= probe << 13; // xorshift + probe ^= (int)((uint)probe >> 17); + probe ^= probe << 5; + return probe; + } + + /** + * Handles cases of updates involving initialization, resizing, + * creating new Cells, and/or contention. See above for + * explanation. This method suffers the usual non-modularity + * problems of optimistic retry code, relying on rechecked sets of + * reads. + * + * @param x the value + * @param wasUncontended false if CAS failed before call + */ + protected void LongAccumulate(long x, bool wasUncontended) + { + var h = GetProbe(); + + var collide = false; // True if last slot nonempty + for (; ; ) + { + Cell[]? @as; Cell a; int n; long v; + if ((@as = this.Cells) != null && (n = @as.Length) > 0) + { + if ((a = @as[(n - 1) & h]) == null) + { + if (this.cellsBusy == 0) + { // Try to attach new Cell + var r = new Cell(x); // Optimistically create + if (this.cellsBusy == 0 && CasCellsBusy()) + { + try + { // Recheck under lock + Cell[]? rs; int m, j; + if ((rs = this.Cells) != null && + (m = rs.Length) > 0 && + rs[j = (m - 1) & h] == null) + { + rs[j] = r; + break; + } + } + finally + { + VolatileWriteNotBusy(); + } + + continue; // Slot is now non-empty + } + } + collide = false; + } + else if (!wasUncontended) // CAS already known to fail + wasUncontended = true; // Continue after rehash + else if (a.value.CompareAndSwap(v = a.value.VolatileRead(), v + x)) + break; + else if (n >= MaxBuckets || this.Cells != @as) + collide = false; // At max size or stale + else if (!collide) + collide = true; + else if (this.cellsBusy == 0 && CasCellsBusy()) + { + try + { + if (this.Cells == @as) + { // Expand table unless stale + var rs = new Cell[n << 1]; + for (var i = 0; i < n; ++i) + rs[i] = @as[i]; + this.Cells = rs; + } + } + finally + { + VolatileWriteNotBusy(); + } + collide = false; + continue; // Retry with expanded table + } + h = AdvanceProbe(h); // Rehash + } + else if (this.cellsBusy == 0 && this.Cells == @as && CasCellsBusy()) + { + try + { // Initialize table + if (this.Cells == @as) + { + var rs = new Cell[2]; + rs[h & 1] = new Cell(x); + this.Cells = rs; + break; + } + } + finally + { + VolatileWriteNotBusy(); + } + } + // Fall back on using base + else if (this.@base.CompareAndSwap(v = this.@base.VolatileRead(), v + x)) + break; + } + } + } +} From 200cbaf5cc1e1cee88bd4a53e1056af2ee2a704f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 05:55:09 +0000 Subject: [PATCH 4/8] Make Counter.Deltas readonly Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- BitFaster.Caching/Counters/Counter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Counters/Counter.cs b/BitFaster.Caching/Counters/Counter.cs index 630b9ee5..e76f64df 100644 --- a/BitFaster.Caching/Counters/Counter.cs +++ b/BitFaster.Caching/Counters/Counter.cs @@ -12,7 +12,7 @@ namespace BitFaster.Caching.Counters public sealed class Counter : Striped64 { - private PaddedLong[] Deltas = new PaddedLong[Environment.ProcessorCount]; + private readonly PaddedLong[] Deltas = new PaddedLong[Environment.ProcessorCount]; /// /// Increment by 1. From 0ee6e899559f0233bcef122e2e62e1941c7d63db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:09:01 +0000 Subject: [PATCH 5/8] Compute Counter sum without resetting deltas Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- BitFaster.Caching/Counters/Counter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Counters/Counter.cs b/BitFaster.Caching/Counters/Counter.cs index e76f64df..bca269b6 100644 --- a/BitFaster.Caching/Counters/Counter.cs +++ b/BitFaster.Caching/Counters/Counter.cs @@ -41,7 +41,7 @@ public long Count() long delta = 0; foreach (ref PaddedLong i in Deltas.AsSpan()) { - delta += Interlocked.Exchange(ref i.value, 0); + delta += Interlocked.Read(ref i.value); } return delta; } From 2e125071d093d284510c6517ea68adaf49eb4d78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:14:06 +0000 Subject: [PATCH 6/8] Use PaddedLong.VolatileRead in Counter.Count for half-fence read Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- BitFaster.Caching/Counters/Counter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Counters/Counter.cs b/BitFaster.Caching/Counters/Counter.cs index bca269b6..23db70ed 100644 --- a/BitFaster.Caching/Counters/Counter.cs +++ b/BitFaster.Caching/Counters/Counter.cs @@ -41,7 +41,7 @@ public long Count() long delta = 0; foreach (ref PaddedLong i in Deltas.AsSpan()) { - delta += Interlocked.Read(ref i.value); + delta += i.VolatileRead(); } return delta; } From 199da0e7f21dee974ff9cf8c741b837d00f67679 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 06:53:04 +0000 Subject: [PATCH 7/8] Remove nullable annotations in Striped64Counter to fix CS8632 warnings Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- BitFaster.Caching.Benchmarks/Striped64Counter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BitFaster.Caching.Benchmarks/Striped64Counter.cs b/BitFaster.Caching.Benchmarks/Striped64Counter.cs index df654a69..06da6f5b 100644 --- a/BitFaster.Caching.Benchmarks/Striped64Counter.cs +++ b/BitFaster.Caching.Benchmarks/Striped64Counter.cs @@ -112,7 +112,7 @@ public void Increment() /// The value to add. public void Add(long value) { - Cell[]? @as; + Cell[] @as; long b, v; int m; Cell a; @@ -158,7 +158,7 @@ public Cell(long x) /// /// When non-null, size is a power of 2. /// - protected Cell[]? Cells; + protected Cell[] Cells; /** * Returns the probe value for the current thread. @@ -218,7 +218,7 @@ protected void LongAccumulate(long x, bool wasUncontended) var collide = false; // True if last slot nonempty for (; ; ) { - Cell[]? @as; Cell a; int n; long v; + Cell[] @as; Cell a; int n; long v; if ((@as = this.Cells) != null && (n = @as.Length) > 0) { if ((a = @as[(n - 1) & h]) == null) @@ -230,7 +230,7 @@ protected void LongAccumulate(long x, bool wasUncontended) { try { // Recheck under lock - Cell[]? rs; int m, j; + Cell[] rs; int m, j; if ((rs = this.Cells) != null && (m = rs.Length) > 0 && rs[j = (m - 1) & h] == null) From b0605b043ff3be360c6910ebbe20f19b7e92f1c8 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Wed, 26 Aug 2026 09:40:37 -0700 Subject: [PATCH 8/8] power of 2 --- BitFaster.Caching/Counters/Counter.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/BitFaster.Caching/Counters/Counter.cs b/BitFaster.Caching/Counters/Counter.cs index 23db70ed..faf318e9 100644 --- a/BitFaster.Caching/Counters/Counter.cs +++ b/BitFaster.Caching/Counters/Counter.cs @@ -12,7 +12,17 @@ namespace BitFaster.Caching.Counters public sealed class Counter : Striped64 { - private readonly PaddedLong[] Deltas = new PaddedLong[Environment.ProcessorCount]; + private readonly PaddedLong[] Deltas; + private readonly uint mask; + + /// + /// Creates a new Counter with an intial sum of zero. + /// + public Counter() + { + this.Deltas = new PaddedLong[BitOps.CeilingPowerOfTwo(Environment.ProcessorCount)]; + this.mask = (uint)Deltas.Length - 1; + } /// /// Increment by 1. @@ -28,7 +38,7 @@ public void Increment() /// The value to add. public void Add(long value) { - ref PaddedLong delta = ref Deltas[(uint)Thread.GetCurrentProcessorId() % (uint)Deltas.Length]; + ref PaddedLong delta = ref Deltas[(uint)Thread.GetCurrentProcessorId() & mask]; Interlocked.Add(ref delta.value, value); }