Skip to content

Add the new Lock type to Concurrent Dicionary - #104575

Open
eduardo-vp wants to merge 2 commits into
dotnet:mainfrom
eduardo-vp:add-new-lock-concurrent-dictionary
Open

Add the new Lock type to Concurrent Dicionary#104575
eduardo-vp wants to merge 2 commits into
dotnet:mainfrom
eduardo-vp:add-new-lock-concurrent-dictionary

Conversation

@eduardo-vp

Copy link
Copy Markdown
Member

No description provided.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

for (int i = 0; i < locks.Length; i++)
{
locks[i] = new object();
locks[i] = new Lock();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now allocating an extra object. Is it worth it?

@eduardo-vp eduardo-vp Jul 16, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, seems like I had some error while collecting the data, I updated the comment above

@danmoseley

Copy link
Copy Markdown
Contributor

What is the value of this change?

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

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
    }
  ]
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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): AcquireFirstLock and AcquirePostFirstLock lost their Monitor.IsEntered(locks[0]) assertions. Lock.IsHeldByCurrentThread is 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 extra Lock allocation per dictionary construction. This is inherent to using a typed Lock[] 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

3 participants