Add the new Lock type to Concurrent Dicionary - #104575
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-collections |
| for (int i = 0; i < locks.Length; i++) | ||
| { | ||
| locks[i] = new object(); | ||
| locks[i] = new Lock(); |
There was a problem hiding this comment.
This is now allocating an extra object. Is it worth it?
There was a problem hiding this comment.
The microbenchmarks from the performance repo that use ConcurrentDictionary were tested with this change and got the following results:
// * Summary *
BenchmarkDotNet v0.13.13-nightly.20240311.145, Windows 11 (10.0.22631.3880/23H2/2023Update/SunValley3) (Hyper-V)
AMD EPYC 7763, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.100-preview.2.24157.14
[Host] : .NET 9.0.0 (9.0.24.12805), X64 RyuJIT AVX2
Job-DHIZMB : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-XIGAPM : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20
MinIterationCount=15 WarmupCount=1
| Type | Method | Job | Toolchain | Count | Size | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|--------------------------------- |--------------------- |----------- |------------------------- |------ |----- |------------:|----------:|----------:|------------:|------------:|------------:|------:|--------:|-------:|-------:|----------:|------------:|
| CtorDefaultSize<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | ? | 146.9 ns | 1.90 ns | 1.69 ns | 146.9 ns | 144.5 ns | 150.6 ns | 1.00 | 0.02 | 0.0599 | - | 1008 B | 1.00 |
| CtorDefaultSize<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | ? | 154.4 ns | 2.94 ns | 2.60 ns | 154.7 ns | 150.6 ns | 158.6 ns | 1.05 | 0.02 | 0.0766 | - | 1288 B | 1.28 |
| | | | | | | | | | | | | | | | | | |
| CtorDefaultSize<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | ? | 153.5 ns | 2.50 ns | 2.34 ns | 152.9 ns | 149.0 ns | 157.1 ns | 1.00 | 0.02 | 0.0601 | - | 1008 B | 1.00 |
| CtorDefaultSize<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | ? | 167.7 ns | 2.21 ns | 1.96 ns | 167.7 ns | 164.0 ns | 171.7 ns | 1.09 | 0.02 | 0.0768 | - | 1288 B | 1.28 |
| | | | | | | | | | | | | | | | | | |
| TryAddDefaultSize<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | 512 | ? | 35,559.7 ns | 142.64 ns | 111.36 ns | 35,587.8 ns | 35,336.2 ns | 35,691.1 ns | 1.00 | 0.00 | 5.7078 | 0.9989 | 96672 B | 1.00 |
| TryAddDefaultSize<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | 512 | ? | 33,709.7 ns | 193.49 ns | 161.57 ns | 33,719.4 ns | 33,359.2 ns | 33,906.9 ns | 0.95 | 0.01 | 6.2633 | 1.4659 | 104888 B | 1.08 |
| | | | | | | | | | | | | | | | | | |
| TryAddDefaultSize<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | 512 | ? | 47,032.4 ns | 426.37 ns | 356.03 ns | 46,839.7 ns | 46,595.5 ns | 47,572.0 ns | 1.00 | 0.01 | 6.1937 | 1.5015 | 105736 B | 1.00 |
| TryAddDefaultSize<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | 512 | ? | 45,092.8 ns | 361.32 ns | 301.71 ns | 45,087.3 ns | 44,725.2 ns | 45,851.2 ns | 0.96 | 0.01 | 6.6643 | 1.6210 | 113952 B | 1.08 |
| | | | | | | | | | | | | | | | | | |
| TryAddGiventSize<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | 512 | ? | 24,125.2 ns | 160.18 ns | 141.99 ns | 24,149.0 ns | 23,912.9 ns | 24,347.5 ns | 1.00 | 0.01 | 3.1342 | 0.4609 | 53192 B | 1.00 |
| TryAddGiventSize<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | 512 | ? | 21,889.2 ns | 87.38 ns | 77.46 ns | 21,894.8 ns | 21,766.5 ns | 22,016.3 ns | 0.91 | 0.01 | 3.1381 | 0.4358 | 53280 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| TryAddGiventSize<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | 512 | ? | 33,368.4 ns | 280.54 ns | 248.69 ns | 33,312.0 ns | 33,023.7 ns | 33,729.8 ns | 1.00 | 0.01 | 3.6058 | 0.6677 | 60624 B | 1.00 |
| TryAddGiventSize<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | 512 | ? | 31,855.5 ns | 141.53 ns | 118.19 ns | 31,862.5 ns | 31,674.6 ns | 32,023.8 ns | 0.95 | 0.01 | 3.5497 | 0.5071 | 60712 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| ContainsKeyFalse<Int32, Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 2,137.6 ns | 2.29 ns | 2.03 ns | 2,137.5 ns | 2,133.5 ns | 2,141.5 ns | 1.00 | 0.00 | - | - | - | NA |
| ContainsKeyFalse<Int32, Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 2,144.4 ns | 12.83 ns | 10.72 ns | 2,141.1 ns | 2,135.6 ns | 2,171.0 ns | 1.00 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| ContainsKeyFalse<String, String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 6,749.0 ns | 60.43 ns | 56.52 ns | 6,725.9 ns | 6,700.9 ns | 6,892.5 ns | 1.00 | 0.01 | - | - | - | NA |
| ContainsKeyFalse<String, String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 6,702.5 ns | 7.91 ns | 6.61 ns | 6,702.5 ns | 6,692.5 ns | 6,714.9 ns | 0.99 | 0.01 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| ContainsKeyTrue<Int32, Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 2,328.8 ns | 5.03 ns | 4.46 ns | 2,328.0 ns | 2,323.2 ns | 2,339.0 ns | 1.00 | 0.00 | - | - | - | NA |
| ContainsKeyTrue<Int32, Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 2,323.4 ns | 7.95 ns | 7.05 ns | 2,324.8 ns | 2,302.4 ns | 2,330.3 ns | 1.00 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| ContainsKeyTrue<String, String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 9,051.3 ns | 12.19 ns | 10.18 ns | 9,051.3 ns | 9,036.7 ns | 9,071.9 ns | 1.00 | 0.00 | - | - | - | NA |
| ContainsKeyTrue<String, String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 9,166.5 ns | 26.97 ns | 21.06 ns | 9,163.3 ns | 9,126.9 ns | 9,215.3 ns | 1.01 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| TryGetValueFalse<Int32, Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 809.2 ns | 3.61 ns | 3.38 ns | 807.7 ns | 805.3 ns | 816.4 ns | 1.00 | 0.01 | - | - | - | NA |
| TryGetValueFalse<Int32, Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 757.5 ns | 2.76 ns | 2.31 ns | 757.8 ns | 752.8 ns | 760.9 ns | 0.94 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| TryGetValueFalse<String, String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 5,382.0 ns | 9.90 ns | 8.77 ns | 5,379.7 ns | 5,371.4 ns | 5,400.3 ns | 1.00 | 0.00 | - | - | - | NA |
| TryGetValueFalse<String, String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 5,368.2 ns | 11.25 ns | 9.97 ns | 5,365.8 ns | 5,354.6 ns | 5,384.0 ns | 1.00 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| TryGetValueTrue<Int32, Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 1,028.3 ns | 5.74 ns | 4.79 ns | 1,029.6 ns | 1,018.0 ns | 1,034.2 ns | 1.00 | 0.01 | - | - | - | NA |
| TryGetValueTrue<Int32, Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 1,018.1 ns | 7.80 ns | 6.92 ns | 1,018.3 ns | 1,006.8 ns | 1,030.0 ns | 0.99 | 0.01 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| TryGetValueTrue<String, String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 6,856.2 ns | 27.24 ns | 25.48 ns | 6,846.1 ns | 6,832.0 ns | 6,923.0 ns | 1.00 | 0.01 | - | - | - | NA |
| TryGetValueTrue<String, String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 6,853.1 ns | 17.52 ns | 15.54 ns | 6,848.5 ns | 6,832.4 ns | 6,882.9 ns | 1.00 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| CtorGivenSize<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 195.4 ns | 5.16 ns | 5.94 ns | 194.8 ns | 184.9 ns | 208.3 ns | 1.00 | 0.04 | 0.2658 | 0.0041 | 4448 B | 1.00 |
| CtorGivenSize<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 198.8 ns | 4.99 ns | 5.75 ns | 199.7 ns | 188.8 ns | 207.0 ns | 1.02 | 0.04 | 0.2705 | 0.0041 | 4536 B | 1.02 |
| | | | | | | | | | | | | | | | | | |
| CtorGivenSize<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 200.3 ns | 4.96 ns | 5.71 ns | 201.2 ns | 188.5 ns | 209.8 ns | 1.00 | 0.04 | 0.2651 | 0.0041 | 4448 B | 1.00 |
| CtorGivenSize<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 208.5 ns | 5.94 ns | 6.84 ns | 209.4 ns | 197.8 ns | 225.5 ns | 1.04 | 0.04 | 0.2707 | 0.0041 | 4536 B | 1.02 |
| | | | | | | | | | | | | | | | | | |
| AddGivenSize<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 23,733.1 ns | 173.66 ns | 145.01 ns | 23,736.0 ns | 23,521.6 ns | 24,027.5 ns | 1.00 | 0.01 | 3.1250 | 0.4735 | 53192 B | 1.00 |
| AddGivenSize<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 21,869.5 ns | 172.93 ns | 144.41 ns | 21,836.5 ns | 21,589.6 ns | 22,208.2 ns | 0.92 | 0.01 | 3.1557 | 0.4383 | 53280 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| AddGivenSize<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 34,198.4 ns | 496.09 ns | 464.04 ns | 34,005.0 ns | 33,664.4 ns | 35,291.5 ns | 1.00 | 0.02 | 3.6212 | 0.6706 | 60624 B | 1.00 |
| AddGivenSize<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 31,877.2 ns | 127.37 ns | 99.44 ns | 31,882.3 ns | 31,719.9 ns | 32,068.1 ns | 0.93 | 0.01 | 3.5497 | 0.5071 | 60712 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| CtorFromCollection<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 21,349.9 ns | 203.97 ns | 180.81 ns | 21,357.7 ns | 21,086.6 ns | 21,701.8 ns | 1.00 | 0.01 | 2.9721 | 0.4246 | 50960 B | 1.00 |
| CtorFromCollection<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 20,783.7 ns | 104.29 ns | 87.09 ns | 20,788.1 ns | 20,611.7 ns | 20,929.2 ns | 0.97 | 0.01 | 2.9880 | 0.4150 | 51240 B | 1.01 |
| | | | | | | | | | | | | | | | | | |
| CtorFromCollection<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 33,567.2 ns | 354.90 ns | 314.61 ns | 33,482.9 ns | 33,278.8 ns | 34,427.2 ns | 1.00 | 0.01 | 3.4722 | 0.5342 | 58568 B | 1.00 |
| CtorFromCollection<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 33,210.4 ns | 263.84 ns | 220.32 ns | 33,133.4 ns | 33,002.3 ns | 33,788.0 ns | 0.99 | 0.01 | 3.4211 | 0.5263 | 58848 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| CreateAddAndClear<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 41,078.2 ns | 178.16 ns | 157.94 ns | 41,072.5 ns | 40,857.0 ns | 41,358.6 ns | 1.00 | 0.01 | 5.8901 | 0.8181 | 99120 B | 1.00 |
| CreateAddAndClear<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 37,487.7 ns | 181.74 ns | 151.76 ns | 37,522.6 ns | 37,251.5 ns | 37,796.3 ns | 0.91 | 0.00 | 6.3836 | 1.4846 | 107336 B | 1.08 |
| | | | | | | | | | | | | | | | | | |
| CreateAddAndClear<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 52,386.4 ns | 423.13 ns | 375.09 ns | 52,289.1 ns | 51,567.3 ns | 53,020.4 ns | 1.00 | 0.01 | 6.4156 | 1.0348 | 108184 B | 1.00 |
| CreateAddAndClear<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 48,391.9 ns | 232.04 ns | 193.76 ns | 48,380.8 ns | 48,053.7 ns | 48,826.8 ns | 0.92 | 0.01 | 6.7724 | 1.1610 | 116400 B | 1.08 |
| | | | | | | | | | | | | | | | | | |
| IterateForEach<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 6,504.7 ns | 12.54 ns | 10.47 ns | 6,504.2 ns | 6,491.8 ns | 6,532.0 ns | 1.00 | 0.00 | - | - | 56 B | 1.00 |
| IterateForEach<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 6,501.1 ns | 9.04 ns | 8.01 ns | 6,499.1 ns | 6,490.1 ns | 6,516.5 ns | 1.00 | 0.00 | - | - | 56 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| IterateForEach<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 11,213.8 ns | 17.22 ns | 15.26 ns | 11,210.4 ns | 11,190.8 ns | 11,239.8 ns | 1.00 | 0.00 | - | - | 64 B | 1.00 |
| IterateForEach<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 11,235.4 ns | 20.17 ns | 17.88 ns | 11,230.3 ns | 11,210.9 ns | 11,270.7 ns | 1.00 | 0.00 | - | - | 64 B | 1.00 |
| | | | | | | | | | | | | | | | | | |
| IndexerSet<Int32> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 12,385.7 ns | 25.35 ns | 22.47 ns | 12,380.9 ns | 12,352.8 ns | 12,436.3 ns | 1.00 | 0.00 | - | - | - | NA |
| IndexerSet<Int32> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 10,775.9 ns | 32.44 ns | 30.35 ns | 10,772.9 ns | 10,713.8 ns | 10,814.7 ns | 0.87 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | | | | | | | |
| IndexerSet<String> | ConcurrentDictionary | Job-DHIZMB | \CD-Baseline\corerun.exe | ? | 512 | 19,120.0 ns | 34.56 ns | 28.86 ns | 19,117.7 ns | 19,070.8 ns | 19,181.9 ns | 1.00 | 0.00 | - | - | - | NA |
| IndexerSet<String> | ConcurrentDictionary | Job-XIGAPM | \CD-Changed\corerun.exe | ? | 512 | 17,607.4 ns | 43.18 ns | 40.39 ns | 17,593.5 ns | 17,547.0 ns | 17,687.8 ns | 0.92 | 0.00 | - | - | - | NA |
// * Warnings *
Environment
Summary -> Benchmark was executed on the virtual machine with Hyper-V hypervisor. Virtualization can affect the measurement result.
// * Hints *
Outliers
CtorDefaultSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (160.56 ns)
CtorDefaultSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (168.01 ns)
CtorDefaultSize<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (180.52 ns)
TryAddDefaultSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 3 outliers were removed (35.95 us..36.51 us)
TryAddDefaultSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (34.30 us, 35.56 us)
TryAddDefaultSize<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (49.20 us, 49.22 us)
TryAddDefaultSize<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (46.25 us, 46.41 us)
TryAddGiventSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (24.90 us)
TryAddGiventSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (22.64 us)
TryAddGiventSize<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (34.48 us)
TryAddGiventSize<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (32.38 us, 33.04 us)
ContainsKeyFalse<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (2.16 us)
ContainsKeyFalse<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (2.18 us, 2.28 us)
ContainsKeyFalse<String, String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (6.78 us, 6.89 us)
ContainsKeyTrue<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (2.37 us)
ContainsKeyTrue<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed, 2 outliers were detected (2.31 us, 2.34 us)
ContainsKeyTrue<String, String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (9.11 us, 9.13 us)
ContainsKeyTrue<String, String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 3 outliers were removed (9.29 us..9.37 us)
TryGetValueFalse<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (768.02 ns, 778.45 ns)
TryGetValueFalse<String, String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (5.47 us)
TryGetValueFalse<String, String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (5.44 us)
TryGetValueTrue<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (1.06 us, 1.14 us)
TryGetValueTrue<Int32, Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (1.04 us)
TryGetValueTrue<String, String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (6.95 us)
AddGivenSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (24.30 us, 24.34 us)
AddGivenSize<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (22.26 us, 22.53 us)
AddGivenSize<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 3 outliers were removed (32.46 us..33.00 us)
CtorFromCollection<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (22.65 us)
CtorFromCollection<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (21.10 us, 21.85 us)
CtorFromCollection<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (34.69 us)
CtorFromCollection<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (34.03 us, 34.27 us)
CreateAddAndClear<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (42.19 us)
CreateAddAndClear<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (37.98 us, 38.88 us)
CreateAddAndClear<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (54.51 us)
CreateAddAndClear<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (49.32 us, 50.11 us)
IterateForEach<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (6.54 us, 6.54 us)
IterateForEach<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (6.54 us)
IterateForEach<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (11.28 us)
IterateForEach<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Changed\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (11.38 us)
IndexerSet<Int32>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 1 outlier was removed (12.64 us)
IndexerSet<String>.ConcurrentDictionary: PowerPlanMode=00000000-0000-0000-0000-000000000000, Toolchain=\CD-Baseline\corerun.exe, IterationTime=250ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> 2 outliers were removed (19.24 us, 19.38 us)
// * Legends *
Count : Value of the 'Count' parameter
Size : Value of the 'Size' parameter
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Median : Value separating the higher half of all measurements (50th percentile)
Min : Minimum
Max : Maximum
Ratio : Mean of the ratio distribution ([Current]/[Baseline])
RatioSD : Standard deviation of the ratio distribution ([Current]/[Baseline])
Gen0 : GC Generation 0 collects per 1000 operations
Gen1 : GC Generation 1 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
Alloc Ratio : Allocated memory ratio distribution ([Current]/[Baseline])
1 ns : 1 Nanosecond (0.000000001 sec)
The following test case was also used to test the average number of accesses per unit of time the ConcurrentDictionary can handle with this change:
using System.Collections.Concurrent;
using System.Diagnostics;
const int KeyPoolCount = 128 << 10;
const int KeyPoolMask = KeyPoolCount - 1;
const int KeyCount = 4 << 10;
const int KeyMask = KeyCount - 1;
// Populate the key pool with unique nonzero keys. Zero is reserved for ownership.
var keyPool = new int[KeyPoolCount];
for (int i = 0; i < KeyPoolCount; i++)
keyPool[i] = i + 1;
// Populate the keys that will exist in the CD with unique random keys from the pool. The pool entries from which the
// keys are taken are replaced with zeroes.
int rngSeed;
if (args.Length == 0)
{
rngSeed = new Random().Next();
Console.WriteLine($"Rng seed: {rngSeed}");
}
else
rngSeed = (int)uint.Parse(args[0]);
var rng = new Random(rngSeed);
var keys = new int[KeyCount];
for (int i = 0; i < KeyCount; i++)
{
int keyPoolIndex = rng.Next() & KeyPoolMask;
int key = keyPool[keyPoolIndex];
if (key == 0)
{
for (int j = keyPoolIndex + 1; ; j++)
{
keyPoolIndex = j & KeyPoolMask;
key = keyPool[keyPoolIndex];
if (key != 0)
break;
}
}
keyPool[keyPoolIndex] = 0;
keys[i] = key;
}
// Populate the CD with the unique keys
var cd = new ConcurrentDictionary<int, int>();
for (int i = 0; i < KeyCount; i++)
{
if (!cd.TryAdd(keys[i], 0))
throw new InvalidOperationException();
}
int threadCount = Environment.ProcessorCount;
var threadIterationCounters = new int[(threadCount + 1) * 32];
ParameterizedThreadStart threadMain = data =>
{
var tupleData = (Tuple<int, int>)data;
int threadIndex = tupleData.Item1;
int rngSeed = tupleData.Item2;
var rng = new Random(rngSeed);
ref int threadIterationCounter = ref threadIterationCounters[(threadIndex + 1) * 32];
var localKeyPool = keyPool;
var localKeys = keys;
var localCd = cd;
while (true)
{
int randomInt = rng.Next();
// Select a new key and take ownership of the key pool entry
int keyPoolIndex = randomInt & KeyPoolMask;
int newKey = Interlocked.Exchange(ref localKeyPool[keyPoolIndex], 0);
if (newKey == 0)
{
for (int j = keyPoolIndex + 1; ; j++)
{
keyPoolIndex = j & KeyPoolMask;
newKey = Interlocked.Exchange(ref localKeyPool[keyPoolIndex], 0);
if (newKey != 0)
break;
}
}
// Select an existing key and take ownership of the existing key entry
int keyIndex = randomInt & KeyMask;
int key = Interlocked.Exchange(ref localKeys[keyIndex], 0);
if (key == 0)
{
for (int j = keyIndex + 1; ; j++)
{
keyIndex = j & KeyMask;
key = Interlocked.Exchange(ref localKeys[keyIndex], 0);
if (key != 0)
break;
}
}
if (!cd.TryRemove(key, out _) || !cd.TryAdd(newKey, 0))
throw new InvalidOperationException();
// Release the keys and ownerships
localKeyPool[keyPoolIndex] = key;
localKeys[keyIndex] = newKey;
threadIterationCounter++;
}
};
for (int i = 0; i < threadCount; i++)
{
var t = new Thread(threadMain);
t.IsBackground = true;
t.Start(new Tuple<int, int>(i, rng.Next()));
}
var sw = new Stopwatch();
// Warmup
Thread.Sleep(6000);
// Measurements
int totalIterations = 0;
double totalDurationMs = 0;
for (int i = 0; i < 8; i++)
{
int startIterations = 0;
int endIterations = 0;
for (int j = 0; j < threadCount; j++)
startIterations += threadIterationCounters[(j + 1) * 32];
sw.Restart();
Thread.Sleep(1000);
for (int j = 0; j < threadCount; j++)
endIterations += threadIterationCounters[(j + 1) * 32];
sw.Stop();
int iterations = endIterations - startIterations;
double durationMs = sw.Elapsed.TotalMilliseconds;
Console.WriteLine($"Iterations/ms: {iterations / durationMs,10:0.0}");
totalIterations += iterations;
totalDurationMs += durationMs;
}
Console.WriteLine($"Average iter/ms: {totalIterations / totalDurationMs,10:0.0}");After running the test several times, the results were the following:
| Baseline | Changed | Diff (%) | |
|---|---|---|---|
| Average iterations/ms | 40486.8 | 40549.2 | 0.15% |
The tests so far show that the performance of ConcurrentDictionary with these changes is relatively fine.
There was a problem hiding this comment.
The CtorDefaultSize<Int32> shows exactly the same allocation before/after. How is that possible? The new version is obviously allocating one more Lock object than it was before. The Lock objects are also bigger than the System.Objects that were being used before. How should I think about this?
There was a problem hiding this comment.
My bad, seems like I had some error while collecting the data, I updated the comment above
|
What is the value of this change? |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "04962b0154282f462e64425f59861a1b7f48bb4e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "8181785b2fd7a1f01f2dfbcdd826956c029ee6d0",
"last_reviewed_commit": "04962b0154282f462e64425f59861a1b7f48bb4e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "8181785b2fd7a1f01f2dfbcdd826956c029ee6d0",
"last_recorded_worker_run_id": "29673725910",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "04962b0154282f462e64425f59861a1b7f48bb4e",
"review_id": 4730109276
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: ConcurrentDictionary<TKey,TValue> historically guarded its buckets with an object[] array of monitor-lock objects, entered via Monitor.Enter/Monitor.Exit. Since .NET 9 introduced the dedicated System.Threading.Lock type, which is faster and more explicit than monitor locking on arbitrary objects, this PR migrates the internal lock array to Lock[]. The change is internal-only; there is no public API or behavioral surface change.
Approach: The _locks field on the nested Tables type changes from object[] to Lock[], and all producers/consumers are updated accordingly. Lock acquisition is modernized: Monitor.Enter/Exit becomes Lock.Enter()/Exit(); the try/finally + bool lockTaken patterns in TryAddInternal and the alternate-key TryAdd become using (locks[lockNo].EnterScope()) (and using (acquireLock ? locks[lockNo].EnterScope() : default) for the conditional case); and lock (locks[lockNo]) statements now bind to a Lock, which the compiler lowers to EnterScope. Array growth in GrowTable and initial construction allocate new Lock() instances. One micro-optimization is intentionally dropped: the constructor previously reused the locks array itself as locks[0] to avoid an allocation, which is no longer possible with a strongly typed Lock[], so element 0 is now a distinct new Lock().
Both the source project and its dependencies target $(NetCoreAppCurrent), so System.Threading.Lock is available; there is no down-level TFM that would break. The lock-numbering invariants (lock 0 special-casing for resize, snapshotting _locks after acquiring lock 0, releasing [0, locksAcquired)) are preserved unchanged, and using/EnterScope provides the same exception-safety the previous try/finally did.
Summary: This is a clean, correct, well-scoped modernization that preserves the existing locking semantics while adopting the purpose-built Lock type. The only substantive nits are that two Debug.Assert invariants using Monitor.IsEntered on lock 0 were removed rather than translated to the equivalent Lock.IsHeldByCurrentThread; restoring them (noted inline) would retain the debug-time guards. Correctness, thread-safety, and exception-safety are otherwise intact. Worth confirming CI (including the System.Collections.Concurrent stress/functional tests) is green, and note the reviewer question on the PR about the concrete value/perf motivation for the change.
Detailed Findings
- Dropped debug invariants (minor):
AcquireFirstLockandAcquirePostFirstLocklost theirMonitor.IsEntered(locks[0])assertions.Lock.IsHeldByCurrentThreadis the direct replacement. See the two inline comments. - Lost array-reuse allocation trick (informational, not actionable): The constructor no longer aliases the array as
locks[0], adding one extraLockallocation per dictionary construction. This is inherent to using a typedLock[]and is negligible; noted only for completeness.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 64.3 AIC · ⌖ 11 AIC · ⊞ 10K
| Debug.Assert(!Monitor.IsEntered(locks[0])); | ||
|
|
||
| Monitor.Enter(locks[0]); | ||
| locks[0].Enter(); |
There was a problem hiding this comment.
The previous Debug.Assert(!Monitor.IsEntered(locks[0])) diagnostic was dropped in this migration. System.Threading.Lock exposes an equivalent check, so consider restoring the invariant as Debug.Assert(!locks[0].IsHeldByCurrentThread); before the Enter() to preserve the debug-time guard that the first lock isn't already held by this thread.
| { | ||
| object[] locks = tables._locks; | ||
| Debug.Assert(Monitor.IsEntered(locks[0])); | ||
| Lock[] locks = tables._locks; |
There was a problem hiding this comment.
Similarly, the Debug.Assert(Monitor.IsEntered(locks[0])) guard that verified the first lock was already held on entry was removed here. Consider restoring it as Debug.Assert(locks[0].IsHeldByCurrentThread); to keep the invariant that AcquirePostFirstLock is only entered while holding lock 0.
No description provided.