Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an additional length check to FrozenDictionary and FrozenSet #92546

Merged
merged 9 commits into from
Dec 11, 2023

Conversation

andrewjsaid
Copy link
Contributor

@andrewjsaid andrewjsaid commented Sep 24, 2023

On construction of the collection, we compute an unsigned long which is effectively 64 boolean flags, each representing the presence of a key string of a particular length (mod 64) in the set of keys.
When reading from the collection, we can exit early if the key being tested does not map to a bit which has been switched on by the original computation. I believe this has similarities to how Bloom Filters work.
This adds a relatively small cost on creation of the collection as well as a possibly negligible cost to each read operation. However it can speed up reads with certain data patterns especially when the difference between the maximum and minimum key length is large but there aren't many different lengths.

Tested via the following benchmark

using System.Collections.Frozen;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher
    .FromAssembly(typeof(Program).Assembly)
    .Run(new[] { "-i" });

public class FrozenSetBenchmarks
{
    private FrozenSet<string> _hashSet = new HashSet<string>
    {
        "cddafs", "cd9660", "iso", "isofs", "iso9660", "fuseiso", "fuseiso9660", "udf", "umview-mod-umfuseiso9660", "aafs", "adfs", "affs", "anoninode", "anon-inode FS", "apfs", "balloon-kvm-fs", "bdevfs", "befs", "bfs", "bootfs", "bpf_fs",
        "btrfs", "btrfs_test", "coh", "daxfs", "drvfs", "efivarfs", "efs", "exfat", "exofs", "ext", "ext2", "ext2_old", "ext3", "ext2/ext3", "ext4", "ext4dev", "f2fs", "fat", "fuseext2", "fusefat", "hfs", "hfs+", "hfsplus", "hfsx", "hostfs",
        "hpfs", "inodefs", "inotifyfs", "jbd", "jbd2", "jffs", "jffs2", "jfs", "lofs", "logfs", "lxfs", "minix (30 char.)", "minix v2 (30 char.)", "minix v2", "minix", "minix_old", "minix2", "minix2v2", "minix2 v2", "minix3", "mlfs", "msdos",
        "nilfs", "nsfs", "ntfs", "ntfs-3g", "ocfs2", "omfs", "overlay", "overlayfs", "pstorefs", "qnx4", "qnx6", "reiserfs", "rpc_pipefs", "sffs", "smackfs", "squashfs", "swap", "sysv", "sysv2", "sysv4", "tracefs", "ubifs", "ufs", "ufscigam",
        "ufs2", "umsdos", "umview-mod-umfuseext2", "v9fs", "vagrant", "vboxfs", "vxfs", "vxfs_olt", "vzfs", "wslfs", "xenix", "xfs", "xia", "xiafs", "xmount", "zfs", "zfs-fuse", "zsmallocfs", "9p", "acfs", "afp", "afpfs", "afs", "aufs", "autofs",
        "autofs4", "beaglefs", "ceph", "cifs", "coda", "coherent", "curlftpfs", "davfs2", "dlm", "eCryptfs", "fhgfs", "flickrfs", "ftp", "fuse", "fuseblk", "fusedav", "fusesmb", "gfsgfs2", "gfs/gfs2", "gfs2", "glusterfs-client",
        "gmailfs", "gpfs", "ibrix", "k-afs", "kafs", "kbfuse", "ltspfs", "lustre", "ncp", "ncpfs", "nfs", "nfs4", "nfsd", "novell", "obexfs", "panfs", "prl_fs", "s3ql", "samba", "smb", "smb2", "smbfs", "snfs", "sshfs", "vmhgfs", "webdav", "wikipediafs",
        "xenfs", "anon_inode", "anon_inodefs", "aptfs", "avfs", "bdev", "binfmt_misc", "cgroup", "cgroupfs", "cgroup2fs", "configfs", "cpuset", "cramfs", "cramfs-wend", "cryptkeeper", "ctfs", "debugfs", "dev", "devfs", "devpts", "devtmpfs", "encfs", "fd",
        "fdesc", "fuse.gvfsd-fuse", "fusectl", "futexfs", "hugetlbfs", "libpam-encfs", "ibpam-mount", "mntfs", "mqueue", "mtpfs", "mythtvfs", "objfs", "openprom", "openpromfs", "pipefs", "plptools", "proc", "pstore", "pytagsfs", "ramfs", "rofs", "romfs",
        "rootfs", "securityfs", "selinux", "selinuxfs", "sharefs", "sockfs", "sysfs", "tmpfs", "udev", "usbdev", "usbdevfs", "gphotofs", "sdcardfs", "usbfs", "usbdevice", "vfat",
    }.ToFrozenSet();

    // No entry has length 17 so the optimization occurs (and hash code is not calculated)
    [Benchmark]
    public void Caught_By_LengthFilter() => _hashSet.Contains("some_drive_name_2");

    // As some entries have length 14 then the length filter does not help
    [Benchmark]
    public void Missed_By_LengthFilter() => _hashSet.Contains("somedrivename2");

}

Results before (control)

Method Mean Error StdDev
Caught_By_LengthFilter 6.262 ns 0.0730 ns 0.0683 ns
Missed_By_LengthFilter 6.154 ns 0.0571 ns 0.0534 ns

Results after

Method Mean Error StdDev
Caught_By_LengthFilter 1.957 ns 0.0198 ns 0.0185 ns
Missed_By_LengthFilter 6.215 ns 0.0505 ns 0.0422 ns

On construction of the collection, we compute an unsigned long which is effectively 64 boolean flags, each representing the presence of a key string of a particular length (mod 64). When reading from the collection, we can exit early if the key being tested does not map to a bit which has been switched on by the original compuation. I believe this has similarities to how Bloom Filters work.
This adds a relatively small cost on creation of the collection as small cost to each read operation. However it can speed up reads with certain data patterns especially when the difference between the maximum and minimum key length is large but there aren't many different lengths.
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Sep 24, 2023
@ghost
Copy link

ghost commented Sep 24, 2023

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

Issue Details

On construction of the collection, we compute an unsigned long which is effectively 64 boolean flags, each representing the presence of a key string of a particular length (mod 64) in the set of keys.
When reading from the collection, we can exit early if the key being tested does not map to a bit which has been switched on by the original computation. I believe this has similarities to how Bloom Filters work.
This adds a relatively small cost on creation of the collection as well as a small cost to each read operation. However it can speed up reads with certain data patterns especially when the difference between the maximum and minimum key length is large but there aren't many different lengths.

Tested via the following benchmark

using System.Collections.Frozen;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher
    .FromAssembly(typeof(Program).Assembly)
    .Run(new[] { "-i" });

public class FrozenSetBenchmarks
{
    private FrozenSet<string> _hashSet = new HashSet<string>
    {
        "cddafs", "cd9660", "iso", "isofs", "iso9660", "fuseiso", "fuseiso9660", "udf", "umview-mod-umfuseiso9660", "aafs", "adfs", "affs", "anoninode", "anon-inode FS", "apfs", "balloon-kvm-fs", "bdevfs", "befs", "bfs", "bootfs", "bpf_fs",
        "btrfs", "btrfs_test", "coh", "daxfs", "drvfs", "efivarfs", "efs", "exfat", "exofs", "ext", "ext2", "ext2_old", "ext3", "ext2/ext3", "ext4", "ext4dev", "f2fs", "fat", "fuseext2", "fusefat", "hfs", "hfs+", "hfsplus", "hfsx", "hostfs",
        "hpfs", "inodefs", "inotifyfs", "jbd", "jbd2", "jffs", "jffs2", "jfs", "lofs", "logfs", "lxfs", "minix (30 char.)", "minix v2 (30 char.)", "minix v2", "minix", "minix_old", "minix2", "minix2v2", "minix2 v2", "minix3", "mlfs", "msdos",
        "nilfs", "nsfs", "ntfs", "ntfs-3g", "ocfs2", "omfs", "overlay", "overlayfs", "pstorefs", "qnx4", "qnx6", "reiserfs", "rpc_pipefs", "sffs", "smackfs", "squashfs", "swap", "sysv", "sysv2", "sysv4", "tracefs", "ubifs", "ufs", "ufscigam",
        "ufs2", "umsdos", "umview-mod-umfuseext2", "v9fs", "vagrant", "vboxfs", "vxfs", "vxfs_olt", "vzfs", "wslfs", "xenix", "xfs", "xia", "xiafs", "xmount", "zfs", "zfs-fuse", "zsmallocfs", "9p", "acfs", "afp", "afpfs", "afs", "aufs", "autofs",
        "autofs4", "beaglefs", "ceph", "cifs", "coda", "coherent", "curlftpfs", "davfs2", "dlm", "eCryptfs", "fhgfs", "flickrfs", "ftp", "fuse", "fuseblk", "fusedav", "fusesmb", "gfsgfs2", "gfs/gfs2", "gfs2", "glusterfs-client",
        "gmailfs", "gpfs", "ibrix", "k-afs", "kafs", "kbfuse", "ltspfs", "lustre", "ncp", "ncpfs", "nfs", "nfs4", "nfsd", "novell", "obexfs", "panfs", "prl_fs", "s3ql", "samba", "smb", "smb2", "smbfs", "snfs", "sshfs", "vmhgfs", "webdav", "wikipediafs",
        "xenfs", "anon_inode", "anon_inodefs", "aptfs", "avfs", "bdev", "binfmt_misc", "cgroup", "cgroupfs", "cgroup2fs", "configfs", "cpuset", "cramfs", "cramfs-wend", "cryptkeeper", "ctfs", "debugfs", "dev", "devfs", "devpts", "devtmpfs", "encfs", "fd",
        "fdesc", "fuse.gvfsd-fuse", "fusectl", "futexfs", "hugetlbfs", "libpam-encfs", "ibpam-mount", "mntfs", "mqueue", "mtpfs", "mythtvfs", "objfs", "openprom", "openpromfs", "pipefs", "plptools", "proc", "pstore", "pytagsfs", "ramfs", "rofs", "romfs",
        "rootfs", "securityfs", "selinux", "selinuxfs", "sharefs", "sockfs", "sysfs", "tmpfs", "udev", "usbdev", "usbdevfs", "gphotofs", "sdcardfs", "usbfs", "usbdevice", "vfat",
    }.ToFrozenSet();

    // No entry has length 17 so the optimization occurs (and hash code is not calculated)
    [Benchmark]
    public void Caught_By_LengthFilter() => _hashSet.Contains("some_drive_name_2");

    // As some entries have length 14 then the length filter does not help
    [Benchmark]
    public void Missed_By_LengthFilter() => _hashSet.Contains("somedrivename2");

}

Results before (control)

Method Mean Error StdDev
Caught_By_LengthFilter 6.262 ns 0.0730 ns 0.0683 ns
Missed_By_LengthFilter 6.154 ns 0.0571 ns 0.0534 ns

Results after

Method Mean Error StdDev
Caught_By_LengthFilter 1.957 ns 0.0198 ns 0.0185 ns
Missed_By_LengthFilter 6.215 ns 0.0505 ns 0.0422 ns
Author: andrewjsaid
Assignees: -
Labels:

area-System.Collections, community-contribution

Milestone: -

@andrewjsaid
Copy link
Contributor Author

@stephentoub @adamsitnik I would greatly appreciate feedback on this PR. Thanks

@adamsitnik adamsitnik self-assigned this Oct 20, 2023
@adamsitnik adamsitnik added the tenet-performance Performance related issue label Oct 20, 2023
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

I like the idea, but we need to make sure that such misses will be common in real life scenarios (with the micro benchmarks from dotnet/performance repo they are not)

@adamsitnik adamsitnik added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 20, 2023
@ghost ghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 20, 2023
@andrewjsaid andrewjsaid force-pushed the frozen-collections-length-filter branch from 8216613 to 39a44df Compare October 20, 2023 18:48
@andrewjsaid
Copy link
Contributor Author

andrewjsaid commented Oct 21, 2023

Side note: I've changed from % 64 to & 0x3F and that seems to have given a significant bump in perf. Would this be a good suggestion for a JIT optimization for mod powers of 2? My understanding doesn't go that deep...

Edit:
I was unable to reproduce these results and have reverted this change. Indeed after many hours digging I found that fgMorphUModToAndSub optimization does exactly this, and it should apply in this case (Integral Range min is in theory zero)
Thus I am reverting this change as % 64 is much more legible.

@MichalPetryka
Copy link
Contributor

Would this be a good suggestion for a JIT optimization for mod powers of 2?

I think the JIT already does it for unsigned integers.

@andrewjsaid
Copy link
Contributor Author

andrewjsaid commented Oct 21, 2023

Thank you for your review and for pointing me to the resource on how to properly run the benchmarks.

Additional Test

To obtain an interesting discussion I added a new microbenchmark designed to showcase the existing length bounds as well as the proposed length filter in this PR. This can be seen in my results as the Count = 20 case. The implementation is a bimodal distribution of lengths and a testing ("notfound") array uniformly distributed over a larger bounds.

Count == 20 ? ValuesGenerator.ArrayOfUniqueStrings(10, minLength: 10, maxLength: 12)
              .Concat(ValuesGenerator.ArrayOfUniqueStrings(10, minLength: 18, maxLength: 20))
              .Concat(ValuesGenerator.ArrayOfUniqueStrings(10 * 2, minLength: 5, maxLength: 25)).ToArray()

Benchmark Results: TryGetValue_*_FrozenDictionary

dotnet run -c Release -f net8.0 --filter System.Collections.Perf_*FrozenDictionary*TryGetValue_*_FrozenDictionary --corerun D:\Code\_forks\dotnet-runtime-main\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe D:\Code\_forks\dotnet-runtime-pr92546\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe

DefaultFrozenDictionary

// * Summary *

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-rc.1.23463.5
  [Host]     : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2
  Job-YITQJR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-LPACKA : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
MaxIterationCount=20  MinIterationCount=15  WarmupCount=1  

|                             Method |        Job |                         ...   Toolchain | Count |          Mean |        Error |       StdDev |        Median |           Min |           Max | Ratio | RatioSD | Allocated | Alloc Ratio |
|----------------------------------- |----------- |-------------------------...------------ |------ |--------------:|-------------:|-------------:|--------------:|--------------:|--------------:|------:|--------:|----------:|------------:|
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |      24.97 ns |     0.160 ns |     0.150 ns |      24.95 ns |      24.62 ns |      25.17 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |      25.83 ns |     0.116 ns |     0.108 ns |      25.80 ns |      25.70 ns |      26.07 ns |  1.03 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |      22.64 ns |     0.082 ns |     0.072 ns |      22.64 ns |      22.47 ns |      22.75 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |      25.28 ns |     0.826 ns |     0.951 ns |      25.36 ns |      24.00 ns |      26.94 ns |  1.12 |    0.03 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    20 |      89.82 ns |     0.593 ns |     0.555 ns |      89.69 ns |      89.18 ns |      90.85 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    20 |     100.14 ns |     0.752 ns |     0.667 ns |     100.31 ns |      97.87 ns |     100.61 ns |  1.11 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    20 |      36.55 ns |     0.237 ns |     0.210 ns |      36.45 ns |      36.38 ns |      37.01 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    20 |      20.19 ns |     0.186 ns |     0.174 ns |      20.09 ns |      20.03 ns |      20.54 ns |  0.55 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |     805.90 ns |     6.611 ns |     5.861 ns |     804.30 ns |     799.51 ns |     818.56 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |     804.85 ns |     4.217 ns |     3.944 ns |     804.65 ns |     797.32 ns |     812.49 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |     705.79 ns |     3.174 ns |     2.969 ns |     705.63 ns |     700.79 ns |     711.30 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |     551.45 ns |     2.837 ns |     2.369 ns |     550.76 ns |     549.34 ns |     557.81 ns |  0.78 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |   8,292.91 ns |    17.361 ns |    14.497 ns |   8,291.58 ns |   8,268.44 ns |   8,318.85 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |   8,503.81 ns |    87.401 ns |    81.755 ns |   8,447.35 ns |   8,421.88 ns |   8,625.60 ns |  1.02 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |   7,807.82 ns |    48.572 ns |    43.058 ns |   7,796.98 ns |   7,765.43 ns |   7,910.40 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |   7,833.96 ns |     9.129 ns |     7.128 ns |   7,834.54 ns |   7,819.31 ns |   7,847.11 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 | 166,442.32 ns | 2,222.728 ns | 2,079.141 ns | 165,712.83 ns | 164,108.22 ns | 170,252.43 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 | 166,934.41 ns |   495.184 ns |   438.968 ns | 166,961.84 ns | 166,322.87 ns | 167,936.70 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |               |              |              |               |               |               |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 | 137,560.34 ns |   191.444 ns |   159.865 ns | 137,569.46 ns | 137,222.31 ns | 137,854.61 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 | 137,271.18 ns |   411.739 ns |   343.821 ns | 137,204.61 ns | 136,708.77 ns | 137,849.95 ns |  1.00 |    0.00 |         - |          NA |

LengthBucketsFrozenDictionary

// * Summary *

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-rc.1.23463.5
  [Host]     : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2
  Job-YITQJR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-LPACKA : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
MaxIterationCount=20  MinIterationCount=15  WarmupCount=1  

|                             Method |        Job |                         ...   Toolchain | Count | ItemsPerBucket |           Mean |       Error |      StdDev |         Median |            Min |            Max | Ratio | RatioSD | Allocated | Alloc Ratio |
|----------------------------------- |----------- |-------------------------...------------ |------ |--------------- |---------------:|------------:|------------:|---------------:|---------------:|---------------:|------:|--------:|----------:|------------:|
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |              1 |      17.051 ns |   0.1124 ns |   0.1051 ns |      17.003 ns |      16.921 ns |      17.222 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |              1 |      17.586 ns |   0.0801 ns |   0.0710 ns |      17.600 ns |      17.418 ns |      17.695 ns |  1.03 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |              1 |       9.226 ns |   0.0170 ns |   0.0159 ns |       9.224 ns |       9.203 ns |       9.257 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |              1 |       9.260 ns |   0.1037 ns |   0.0970 ns |       9.249 ns |       9.014 ns |       9.381 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |              5 |      68.122 ns |   0.0508 ns |   0.0397 ns |      68.130 ns |      68.052 ns |      68.179 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |              5 |      68.118 ns |   0.4575 ns |   0.4280 ns |      68.286 ns |      67.331 ns |      68.602 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |              5 |       9.254 ns |   0.1105 ns |   0.1033 ns |       9.216 ns |       9.003 ns |       9.404 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |              5 |       9.349 ns |   0.0684 ns |   0.0606 ns |       9.369 ns |       9.203 ns |       9.421 ns |  1.01 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |              1 |     158.529 ns |   1.2121 ns |   1.1338 ns |     157.850 ns |     157.186 ns |     160.057 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |              1 |     158.342 ns |   1.2254 ns |   1.1463 ns |     157.711 ns |     157.291 ns |     160.727 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |              1 |      77.110 ns |   0.6343 ns |   0.5623 ns |      77.346 ns |      76.178 ns |      78.190 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |              1 |      80.830 ns |   0.1416 ns |   0.1255 ns |      80.825 ns |      80.556 ns |      81.016 ns |  1.05 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |              5 |     738.821 ns |   3.5803 ns |   2.9897 ns |     739.541 ns |     729.252 ns |     740.972 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |              5 |     743.723 ns |   8.4069 ns |   7.4525 ns |     744.198 ns |     733.248 ns |     758.683 ns |  1.01 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |              5 |      77.440 ns |   0.1740 ns |   0.1453 ns |      77.408 ns |      77.228 ns |      77.791 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |              5 |      80.413 ns |   0.6787 ns |   0.6349 ns |      80.550 ns |      79.447 ns |      81.473 ns |  1.04 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |              1 |   1,893.726 ns |  12.7395 ns |  11.9165 ns |   1,899.450 ns |   1,867.486 ns |   1,901.216 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |              1 |   1,894.195 ns |  13.1695 ns |  11.6745 ns |   1,897.598 ns |   1,870.840 ns |   1,908.120 ns |  1.00 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |              1 |     909.091 ns |   6.2655 ns |   5.2320 ns |     911.204 ns |     898.495 ns |     913.890 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |              1 |     924.270 ns |   2.6464 ns |   2.3459 ns |     924.569 ns |     917.240 ns |     927.068 ns |  1.02 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |              5 |   7,234.050 ns | 142.4371 ns | 164.0308 ns |   7,340.931 ns |   6,951.339 ns |   7,378.571 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |              5 |   8,128.182 ns |  60.8520 ns |  56.9210 ns |   8,128.110 ns |   8,040.620 ns |   8,225.857 ns |  1.13 |    0.03 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |              5 |     836.289 ns |   6.1594 ns |   5.7615 ns |     834.536 ns |     830.136 ns |     850.002 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |              5 |     821.384 ns |   4.1010 ns |   3.8360 ns |     820.912 ns |     816.971 ns |     829.343 ns |  0.98 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 |              1 |  40,089.427 ns | 364.9943 ns | 341.4159 ns |  39,943.941 ns |  39,814.270 ns |  40,771.875 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 |              1 |  40,773.994 ns | 423.0641 ns | 395.7344 ns |  40,610.872 ns |  40,452.686 ns |  41,714.632 ns |  1.02 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 |              1 |  23,332.201 ns | 339.8734 ns | 317.9178 ns |  23,158.020 ns |  23,102.553 ns |  23,898.212 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 |              1 |  24,058.436 ns | 169.9740 ns | 150.6775 ns |  24,089.552 ns |  23,563.686 ns |  24,183.782 ns |  1.03 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 |              5 | 109,533.397 ns | 754.4202 ns | 629.9751 ns | 109,561.198 ns | 108,640.148 ns | 110,657.943 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 |              5 | 111,799.662 ns | 134.5552 ns | 119.2796 ns | 111,744.040 ns | 111,679.241 ns | 112,043.750 ns |  1.02 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |                |                |             |             |                |                |                |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 |              5 |  19,749.094 ns | 235.5901 ns | 208.8445 ns |  19,826.939 ns |  19,414.657 ns |  20,030.893 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 |              5 |  19,663.061 ns | 186.8218 ns | 165.6127 ns |  19,692.721 ns |  19,137.586 ns |  19,793.923 ns |  1.00 |    0.01 |         - |          NA |

SingleCharFrozenDictionary

// * Summary *

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-rc.1.23463.5
  [Host]     : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2
  Job-YITQJR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-LPACKA : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
MaxIterationCount=20  MinIterationCount=15  WarmupCount=1  

|                             Method |        Job |                         ...   Toolchain | Count |         Mean |      Error |     StdDev |       Median |          Min |          Max | Ratio | RatioSD | Allocated | Alloc Ratio |
|----------------------------------- |----------- |-------------------------...------------ |------ |-------------:|-----------:|-----------:|-------------:|-------------:|-------------:|------:|--------:|----------:|------------:|
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |     24.50 ns |   0.160 ns |   0.142 ns |     24.45 ns |     24.36 ns |     24.86 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |     25.17 ns |   0.186 ns |   0.155 ns |     25.11 ns |     25.02 ns |     25.55 ns |  1.03 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |     19.98 ns |   0.258 ns |   0.241 ns |     20.00 ns |     19.63 ns |     20.52 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |     20.96 ns |   0.178 ns |   0.166 ns |     20.88 ns |     20.80 ns |     21.26 ns |  1.05 |    0.02 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |    235.68 ns |   1.681 ns |   1.572 ns |    236.11 ns |    232.95 ns |    237.71 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |    242.44 ns |   1.197 ns |   0.999 ns |    242.83 ns |    240.51 ns |    243.90 ns |  1.03 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |    190.40 ns |   0.952 ns |   0.890 ns |    190.61 ns |    188.24 ns |    191.45 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |    202.45 ns |   0.664 ns |   0.589 ns |    202.29 ns |    201.49 ns |    203.36 ns |  1.06 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |  2,339.64 ns |   8.421 ns |   7.877 ns |  2,340.76 ns |  2,323.22 ns |  2,351.78 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |  2,354.80 ns |   4.115 ns |   3.849 ns |  2,353.58 ns |  2,349.43 ns |  2,362.32 ns |  1.01 |    0.00 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |  1,867.50 ns |   3.928 ns |   3.482 ns |  1,867.47 ns |  1,862.36 ns |  1,874.33 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |  1,990.94 ns |   3.460 ns |   2.889 ns |  1,991.66 ns |  1,986.63 ns |  1,996.39 ns |  1.07 |    0.00 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 | 23,354.93 ns | 203.478 ns | 190.334 ns | 23,262.98 ns | 23,158.25 ns | 23,629.18 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 | 24,289.50 ns |  28.863 ns |  26.998 ns | 24,286.51 ns | 24,252.43 ns | 24,339.70 ns |  1.04 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 | 19,289.76 ns | 162.407 ns | 151.915 ns | 19,387.59 ns | 19,067.06 ns | 19,448.46 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 | 20,057.21 ns |  25.538 ns |  22.639 ns | 20,059.53 ns | 20,025.77 ns | 20,112.01 ns |  1.04 |    0.01 |         - |          NA |

SubstringFrozenDictionary

// * Summary *

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-rc.1.23463.5
  [Host]     : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2
  Job-YITQJR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-LPACKA : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
MaxIterationCount=20  MinIterationCount=15  WarmupCount=1  

|                             Method |        Job |                         ...   Toolchain | Count |         Mean |      Error |     StdDev |       Median |          Min |          Max | Ratio | RatioSD | Allocated | Alloc Ratio |
|----------------------------------- |----------- |-------------------------...------------ |------ |-------------:|-----------:|-----------:|-------------:|-------------:|-------------:|------:|--------:|----------:|------------:|
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |     46.11 ns |   0.343 ns |   0.304 ns |     46.21 ns |     45.42 ns |     46.55 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |     44.60 ns |   0.245 ns |   0.229 ns |     44.63 ns |     44.28 ns |     45.02 ns |  0.97 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |    10 |     40.55 ns |   0.071 ns |   0.067 ns |     40.56 ns |     40.40 ns |     40.66 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |    10 |     41.85 ns |   0.049 ns |   0.046 ns |     41.85 ns |     41.76 ns |     41.91 ns |  1.03 |    0.00 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |    444.12 ns |   3.977 ns |   3.720 ns |    443.02 ns |    440.19 ns |    451.61 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |    453.88 ns |   8.815 ns |   8.658 ns |    454.26 ns |    441.13 ns |    467.53 ns |  1.02 |    0.02 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |   100 |    399.97 ns |   1.044 ns |   0.977 ns |    400.24 ns |    398.00 ns |    401.21 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |   100 |    420.65 ns |   1.826 ns |   1.708 ns |    419.82 ns |    418.85 ns |    424.02 ns |  1.05 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |  4,627.32 ns |  14.457 ns |  12.072 ns |  4,631.67 ns |  4,593.07 ns |  4,641.21 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |  4,700.16 ns |   9.759 ns |   8.651 ns |  4,700.89 ns |  4,677.82 ns |  4,714.83 ns |  1.02 |    0.00 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe |  1000 |  4,166.23 ns |  19.303 ns |  18.056 ns |  4,168.20 ns |  4,133.46 ns |  4,200.98 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe |  1000 |  4,340.67 ns |  16.809 ns |  15.723 ns |  4,346.82 ns |  4,303.30 ns |  4,358.36 ns |  1.04 |    0.01 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
|  TryGetValue_True_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 | 66,891.61 ns |  92.801 ns |  82.265 ns | 66,883.44 ns | 66,781.08 ns | 67,055.02 ns |  1.00 |    0.00 |         - |          NA |
|  TryGetValue_True_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 | 65,159.61 ns | 256.734 ns | 200.441 ns | 65,219.74 ns | 64,872.19 ns | 65,569.22 ns |  0.97 |    0.00 |         - |          NA |
|                                    |            |                         ...             |       |              |            |            |              |              |              |       |         |           |             |
| TryGetValue_False_FrozenDictionary | Job-YITQJR |     \dotnet-runtime-main...\corerun.exe | 10000 | 52,035.96 ns | 125.014 ns | 110.821 ns | 52,035.99 ns | 51,856.27 ns | 52,196.26 ns |  1.00 |    0.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-LPACKA |  \dotnet-runtime-pr92546...\corerun.exe | 10000 | 52,229.21 ns |  94.308 ns |  83.601 ns | 52,219.33 ns | 52,117.40 ns | 52,394.15 ns |  1.00 |    0.00 |         - |          NA |

Discussion

Two tests have obviously triggered the new check. The DefaultFrozenDictionary TryGetValue_False Count = 20 and Count = 100. This is predictable if one looks at the characteristics of the strings generated by the benchmark. The Count = 10 case generates strings of the same length and the Count >= 1000 case probably generates all string lengths (there are 50 possible lengths). What one should note is that the gains in these cases are very significant (Ratios = 0.55, 0.78). These ratios are highly dependant on the data generated in the benchmark. In fact, I can construct another test case which deliberately showcases this PR, shown below. (11 strings of length 10, 11 strings of length 20, 22 lookups of strings of lengths between 11 and 19). This benchmark shows that we can get a ratio of 0.19.

|                             Method |        Job |                         ...   Toolchain | Count |      Mean |    Error |   StdDev |    Median |       Min |       Max | Ratio | Allocated | Alloc Ratio |
|----------------------------------- |----------- |-------------------------...------------ |------ |----------:|---------:|---------:|----------:|----------:|----------:|------:|----------:|------------:|
| TryGetValue_False_FrozenDictionary | Job-DEUDUN |     \dotnet-runtime-main...\corerun.exe |    22 |  89.56 ns | 0.686 ns | 0.608 ns |  89.84 ns |  88.32 ns |  89.99 ns |  1.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-TGCAPI |  \dotnet-runtime-pr92546...\corerun.exe |    22 |  17.07 ns | 0.302 ns | 0.252 ns |  17.07 ns |  16.76 ns |  17.67 ns |  0.19 |         - |          NA |

[This next section has been edited I looked at the wrong data in the original comment 6 hours ago]
It is interesting to note that when I attempted to further skew the ratio by elongating the strings I actually hit other optimizations (calculating GetHashCode on a substring) as shown below (string lengths are around 10,000).

|                             Method |        Job |                                  ...   Toolchain | Count |     Mean |    Error |   StdDev |   Median |      Min |      Max | Ratio | Allocated | Alloc Ratio |
|----------------------------------- |----------- |----------------------------------...------------ |------ |---------:|---------:|---------:|---------:|---------:|---------:|------:|----------:|------------:|
| TryGetValue_False_FrozenDictionary | Job-LDXIVG |    \dotnet-runtime-main\artifacts...\corerun.exe |    22 | 51.61 ns | 0.435 ns | 0.407 ns | 51.57 ns | 50.83 ns | 52.37 ns |  1.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-RTIYOO | \dotnet-runtime-pr92546\artifacts...\corerun.exe |    22 | 16.72 ns | 0.365 ns | 0.342 ns | 16.60 ns | 16.27 ns | 17.38 ns |  0.32 |         - |          NA |

Note however that while the existing branch changed depending on the calculation of the hash code, the proposed filter is constant (approx 17ns in both of the above benchmarks) as it skips the calculation entirely.

Thus I think we can safely conclude that there is a very small penalty for adding this filter (far less than a nanosecond per method call) but can result in a big gain (skipping computation of a hash code) when it is of use.

In terms of how often this will be used in real-world cases, unfortunately I have no concrete statistics. Is there a way we can obtain any data here besides intuition? How was the cost/benefit for the min/max filter made?

@danmoseley
Copy link
Member

This seems like a good cost/benefit to me. (But I'm not area owner)

Should any of your tests go into dotnet/performance? We don't aim to be exhaustive there, so it would only be if there was a clear gap in coverage.

@andrewjsaid
Copy link
Contributor Author

Should any of your tests go into dotnet/performance?

https://github.com/dotnet/performance/pull/3432/files#r1367705797

Added as a suggestion to @adamsitnik 's PR which importantly adds coverage for the TryGetValue = false scenario.

@andrewjsaid
Copy link
Contributor Author

andrewjsaid commented Oct 21, 2023

One possible realistic scenario where this PR might come in useful is in aspnetcore's DictionaryJumpTable.cs (used in AOT)

One can imagine some routes like the following

POST /api/orders/{id}/add/{item-id}
POST /api/orders/{id}/mark-as-deleted
POST /api/orders/{id}/reset
... (enough other endpoints to avoid `LinearSearchJumpTable`)
POST /api/orders/{id}/{item-id}/quantity

In the above example the frozen dictionary would contain add, mark-as-deleted, reset but potentially many requests would come in for {item-id}.
If {item-id} has a length which none of the other paths have, then the optimisation occurs.

I got the following results by constructing an example data showcasing the above

// * Summary *

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-rc.1.23463.5
  [Host]     : .NET 8.0.0 (8.0.23.41904), X64 RyuJIT AVX2
  Job-YRQHWN : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-JIMCXL : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
MaxIterationCount=20  MinIterationCount=15  WarmupCount=1  

|                             Method |        Job |                         ...   Toolchain | Count |     Mean |    Error |   StdDev |   Median |      Min |      Max | Ratio | Allocated | Alloc Ratio |
|----------------------------------- |----------- |-------------------------...------------ |------ |---------:|---------:|---------:|---------:|---------:|---------:|------:|----------:|------------:|
| TryGetValue_False_FrozenDictionary | Job-YRQHWN |    \dotnet-runtime-main\...\corerun.exe |    11 | 45.61 ns | 0.215 ns | 0.180 ns | 45.56 ns | 45.42 ns | 45.97 ns |  1.00 |         - |          NA |
| TryGetValue_False_FrozenDictionary | Job-JIMCXL | \dotnet-runtime-pr92546\...\corerun.exe |    11 | 11.68 ns | 0.142 ns | 0.133 ns | 11.65 ns | 11.45 ns | 11.89 ns |  0.26 |         - |          NA |

@danmoseley
Copy link
Member

Cc @BrennanConroy for that.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

Hi @andrewjsaid

Thank you for applying my suggestion! I've re run all the frozen collection benchmarks we have with --launchCount 3 argument (each benchmark run in 3 dedicated processes, helps to lower side effects of memory randomization and other similar factors).

Perf_LengthBucketsFrozenDictionary

The TryGetValue_True_FrozenDictionary and TryGetValue_False_FrozenDictionary show no difference (ratio 1.00 or 0.99), which confirms that we can trust the results: there were no changes made to this particular code path and performance is the same.

But the creation of frozen dictionaries has regressed up to 7% for small inputs. I know that my suggestion is the reason for that.

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.100-alpha.1.23531.2
  [Host]     : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
          PR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
        main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
LaunchCount=3  MaxIterationCount=20  MinIterationCount=15  
WarmupCount=1  
Method Job Count ItemsPerBucket Mean Ratio Allocated
ToFrozenDictionary PR 10 1 234.39 ns 1.06 488 B
ToFrozenDictionary main 10 1 220.88 ns 1.00 488 B
TryGetValue_True_FrozenDictionary PR 10 1 22.31 ns 1.00 -
TryGetValue_True_FrozenDictionary main 10 1 22.20 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10 1 12.49 ns 1.00 -
TryGetValue_False_FrozenDictionary main 10 1 12.49 ns 1.00 -
ToFrozenDictionary PR 10 5 231.01 ns 1.02 328 B
ToFrozenDictionary main 10 5 225.83 ns 1.00 328 B
TryGetValue_True_FrozenDictionary PR 10 5 80.77 ns 1.02 -
TryGetValue_True_FrozenDictionary main 10 5 78.98 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10 5 12.47 ns 1.00 -
TryGetValue_False_FrozenDictionary main 10 5 12.47 ns 1.00 -
ToFrozenDictionary PR 100 1 1,110.68 ns 1.06 3728 B
ToFrozenDictionary main 100 1 1,045.52 ns 1.00 3728 B
TryGetValue_True_FrozenDictionary PR 100 1 206.33 ns 1.00 -
TryGetValue_True_FrozenDictionary main 100 1 206.37 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 100 1 102.75 ns 1.00 -
TryGetValue_False_FrozenDictionary main 100 1 102.44 ns 1.00 -
ToFrozenDictionary PR 100 5 1,131.40 ns 1.07 2128 B
ToFrozenDictionary main 100 5 1,055.68 ns 1.00 2128 B
TryGetValue_True_FrozenDictionary PR 100 5 902.55 ns 1.00 -
TryGetValue_True_FrozenDictionary main 100 5 903.75 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 100 5 102.68 ns 1.00 -
TryGetValue_False_FrozenDictionary main 100 5 102.40 ns 1.00 -
ToFrozenDictionary PR 1000 1 9,514.12 ns 1.04 36128 B
ToFrozenDictionary main 1000 1 9,178.91 ns 1.00 36128 B
TryGetValue_True_FrozenDictionary PR 1000 1 2,362.08 ns 1.00 -
TryGetValue_True_FrozenDictionary main 1000 1 2,361.07 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 1000 1 1,253.38 ns 1.00 -
TryGetValue_False_FrozenDictionary main 1000 1 1,251.53 ns 1.00 -
ToFrozenDictionary PR 1000 5 9,501.18 ns 1.02 20128 B
ToFrozenDictionary main 1000 5 9,360.78 ns 1.00 20128 B
TryGetValue_True_FrozenDictionary PR 1000 5 8,324.55 ns 1.00 -
TryGetValue_True_FrozenDictionary main 1000 5 8,301.55 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 1000 5 1,172.01 ns 1.00 -
TryGetValue_False_FrozenDictionary main 1000 5 1,168.92 ns 1.00 -
ToFrozenDictionary PR 10000 1 239,837.56 ns 0.99 360129 B
ToFrozenDictionary main 10000 1 241,305.99 ns 1.00 360129 B
TryGetValue_True_FrozenDictionary PR 10000 1 63,182.66 ns 0.99 -
TryGetValue_True_FrozenDictionary main 10000 1 63,709.90 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10000 1 60,050.12 ns 0.99 -
TryGetValue_False_FrozenDictionary main 10000 1 60,422.24 ns 1.00 -
ToFrozenDictionary PR 10000 5 180,362.34 ns 1.02 200129 B
ToFrozenDictionary main 10000 5 176,522.39 ns 1.00 200129 B
TryGetValue_True_FrozenDictionary PR 10000 5 122,381.94 ns 1.00 -
TryGetValue_True_FrozenDictionary main 10000 5 122,064.66 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10000 5 49,376.62 ns 1.00 -
TryGetValue_False_FrozenDictionary main 10000 5 49,377.70 ns 1.00 -

Perf_SingleCharFrozenDictionary

The creation time has not changed, but TryGetValue has regressed by 3 to 5%. I know that it would have been different if the inputs for TryGetValue_False were strings with more than a single character.

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.100-alpha.1.23531.2
  [Host]     : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
          PR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
        main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
LaunchCount=3  MaxIterationCount=20  MinIterationCount=15  
WarmupCount=1  
Method Job Count Mean Ratio Allocated
ToFrozenDictionary PR 10 688.71 ns 1.00 1440 B
ToFrozenDictionary main 10 687.80 ns 1.00 1432 B
TryGetValue_True_FrozenDictionary PR 10 32.89 ns 1.04 -
TryGetValue_True_FrozenDictionary main 10 31.76 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10 26.43 ns 1.04 -
TryGetValue_False_FrozenDictionary main 10 25.36 ns 1.00 -
ToFrozenDictionary PR 100 4,190.57 ns 0.96 9864 B
ToFrozenDictionary main 100 4,343.67 ns 1.00 9856 B
TryGetValue_True_FrozenDictionary PR 100 321.98 ns 1.03 -
TryGetValue_True_FrozenDictionary main 100 313.56 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 100 258.90 ns 1.04 -
TryGetValue_False_FrozenDictionary main 100 248.27 ns 1.00 -
ToFrozenDictionary PR 1000 43,094.51 ns 1.05 94872 B
ToFrozenDictionary main 1000 41,252.63 ns 1.00 94864 B
TryGetValue_True_FrozenDictionary PR 1000 3,176.23 ns 1.04 -
TryGetValue_True_FrozenDictionary main 1000 3,049.37 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 1000 2,491.99 ns 1.05 -
TryGetValue_False_FrozenDictionary main 1000 2,368.69 ns 1.00 -
ToFrozenDictionary PR 10000 721,583.83 ns 1.00 892490 B
ToFrozenDictionary main 10000 718,446.34 ns 1.00 892482 B
TryGetValue_True_FrozenDictionary PR 10000 31,959.12 ns 1.04 -
TryGetValue_True_FrozenDictionary main 10000 30,790.82 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10000 25,065.95 ns 1.05 -
TryGetValue_False_FrozenDictionary main 10000 23,867.58 ns 1.00 -

Perf_SubstringFrozenDictionary

The creation time has not changed or is within the range of error, but TryGetValue has regressed by 2 to 3%.

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.100-alpha.1.23531.2
  [Host]     : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
          PR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
        main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
LaunchCount=3  MaxIterationCount=20  MinIterationCount=15  
WarmupCount=1  

Method Job Count Mean Ratio Allocated
ToFrozenDictionary PR 10 1,336.32 ns 1.00 1728 B
ToFrozenDictionary main 10 1,331.19 ns 1.00 1720 B
TryGetValue_True_FrozenDictionary PR 10 56.75 ns 1.03 -
TryGetValue_True_FrozenDictionary main 10 55.21 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10 50.66 ns 1.03 -
TryGetValue_False_FrozenDictionary main 10 49.07 ns 1.00 -
ToFrozenDictionary PR 100 9,128.97 ns 1.01 12120 B
ToFrozenDictionary main 100 9,057.09 ns 1.00 12112 B
TryGetValue_True_FrozenDictionary PR 100 578.00 ns 1.02 -
TryGetValue_True_FrozenDictionary main 100 566.76 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 100 509.78 ns 1.03 -
TryGetValue_False_FrozenDictionary main 100 493.32 ns 1.00 -
ToFrozenDictionary PR 1000 66,301.37 ns 1.01 94872 B
ToFrozenDictionary main 1000 65,870.51 ns 1.00 94864 B
TryGetValue_True_FrozenDictionary PR 1000 6,177.06 ns 1.03 -
TryGetValue_True_FrozenDictionary main 1000 6,016.53 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 1000 5,543.15 ns 1.03 -
TryGetValue_False_FrozenDictionary main 1000 5,375.22 ns 1.00 -
ToFrozenDictionary PR 10000 1,265,175.44 ns 1.00 926146 B
ToFrozenDictionary main 10000 1,266,817.49 ns 1.00 926138 B
TryGetValue_True_FrozenDictionary PR 10000 86,572.00 ns 1.03 -
TryGetValue_True_FrozenDictionary main 10000 84,168.82 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10000 65,729.32 ns 1.02 -
TryGetValue_False_FrozenDictionary main 10000 64,286.50 ns 1.00 -

Perf_DefaultFrozenDictionary

The creation time has not changed or is within the range of error. When it comes to TryGetValue it depends: it has regressed by 1 to 7% for some cases, while improved 21% for other.

BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.100-alpha.1.23531.2
  [Host]     : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
          PR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
        main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  IterationTime=250.0000 ms  
LaunchCount=3  MaxIterationCount=20  MinIterationCount=15  
WarmupCount=1  
Method Job Count Mean Ratio Allocated
ToFrozenDictionary PR 10 718.87 ns 1.01 1440 B
ToFrozenDictionary main 10 710.13 ns 1.00 1432 B
TryGetValue_True_FrozenDictionary PR 10 33.10 ns 1.04 -
TryGetValue_True_FrozenDictionary main 10 31.79 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 10 29.78 ns 1.03 -
TryGetValue_False_FrozenDictionary main 10 29.06 ns 1.00 -
ToFrozenDictionary PR 100 10,669.42 ns 0.99 18568 B
ToFrozenDictionary main 100 10,823.51 ns 1.00 18560 B
TryGetValue_True_FrozenDictionary PR 100 1,034.05 ns 1.01 -
TryGetValue_True_FrozenDictionary main 100 1,022.50 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 100 705.22 ns 0.79 -
TryGetValue_False_FrozenDictionary main 100 894.03 ns 1.00 -
ToFrozenDictionary PR 1000 58,759.00 ns 0.99 98616 B
ToFrozenDictionary main 1000 59,278.50 ns 1.00 98608 B
TryGetValue_True_FrozenDictionary PR 1000 10,935.30 ns 1.00 -
TryGetValue_True_FrozenDictionary main 1000 10,902.89 ns 1.00 -
TryGetValue_False_FrozenDictionary PR 1000 10,322.62 ns 1.01 -
TryGetValue_False_FrozenDictionary main 1000 10,228.25 ns 1.00 -
ToFrozenDictionary PR 10000 984,770.98 ns 1.00 926141 B
ToFrozenDictionary main 10000 980,378.44 ns 1.00 926133 B
TryGetValue_True_FrozenDictionary PR 10000 238,425.79 ns 1.01 1 B
TryGetValue_True_FrozenDictionary main 10000 235,672.89 ns 1.00 1 B
TryGetValue_False_FrozenDictionary PR 10000 203,839.76 ns 1.07 1 B
TryGetValue_False_FrozenDictionary main 10000 191,346.29 ns 1.00 1 B

Conclusion

The main idea behind Frozen collection optimizations is finding the right strategy.
We already have a dedicated strategy optimized for length checks (LengthBucketsFrozenDictionary).
The SubstringFrozenDictionary strategy performs a LOT of work to focus only on part of the strings when doing the hashcode computation.
I believe that these two strategies will be used most of the time for ASP.NET paths and similar examples. I don't believe that we should change them.
SingleCharFrozenDictionary will be used in very specific examples and it should not regress.

However, the fallback strategy (DefaultFrozenDictionary) cleary shows that nice gains are possible.

Would it be possible to change the code in a way that the optimization is used only in the default strategy?

Thank you for your contribution!

(cc @stephentoub to verify if he agrees with my conclusions)

@ghost ghost added the needs-author-action An issue or pull request that requires more info or actions from the author. label Nov 17, 2023
@ghost ghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Nov 17, 2023
@andrewjsaid
Copy link
Contributor Author

Thank you for your feedback. I take your point; with the minimal cost of hashing a single char or a few chars, there's not much benefit to be had from filtering early when a few instructions later we will be testing equality which early on compares string lengths anyway.

I've implemented the change to only apply when no hashable substring was found by KeyAnalyzer. Benchmark results below showing what you would expect. Luckily the concrete classes already had been optimized for customized codegen based on overridden methods Equals and GetHashCode.

Perf_DefaultFrozenDictionary

Improvement has remained (~20% in the relevant case).

|                             Method |               Toolchain | Count |          Mean | Ratio | RatioSD |
|----------------------------------- |------------------------ |------ |--------------:|------:|--------:|
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |    10 |      24.92 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |    10 |      24.93 ns |  1.00 |    0.02 |
|                                    |                         |       |               |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |    10 |      22.50 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |    10 |      21.87 ns |  0.97 |    0.05 |
|                                    |                         |       |               |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |   100 |     765.10 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |   100 |     775.43 ns |  1.01 |    0.02 |
|                                    |                         |       |               |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |   100 |     667.79 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |   100 |     539.15 ns |  0.81 |    0.02 |
|                                    |                         |       |               |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |  1000 |   8,060.49 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |  1000 |   8,062.08 ns |  1.00 |    0.02 |
|                                    |                         |       |               |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |  1000 |   7,501.04 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |  1000 |   7,542.41 ns |  1.01 |    0.01 |
|                                    |                         |       |               |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main | 10000 | 162,555.61 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 | 10000 | 165,447.62 ns |  1.02 |    0.01 |
|                                    |                         |       |               |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main | 10000 | 137,994.69 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 | 10000 | 138,603.15 ns |  1.00 |    0.02 |

Perf_LengthBucketsFrozenDictionary

Still no change (just noise)

|                             Method |               Toolchain | Count | ItemsPerBucket |           Mean | Ratio | RatioSD |
|----------------------------------- |------------------------ |------ |--------------- |---------------:|------:|--------:|
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |    10 |              1 |      17.022 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |    10 |              1 |      17.167 ns |  1.01 |    0.02 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |    10 |              1 |       8.611 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |    10 |              1 |       8.583 ns |  1.00 |    0.02 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |    10 |              5 |      56.041 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |    10 |              5 |      56.575 ns |  1.01 |    0.01 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |    10 |              5 |       8.638 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |    10 |              5 |       8.599 ns |  1.00 |    0.03 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |   100 |              1 |     156.655 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |   100 |              1 |     157.464 ns |  1.01 |    0.02 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |   100 |              1 |      78.636 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |   100 |              1 |      78.647 ns |  1.00 |    0.01 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |   100 |              5 |     652.883 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |   100 |              5 |     671.718 ns |  1.03 |    0.04 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |   100 |              5 |      79.549 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |   100 |              5 |      79.696 ns |  1.00 |    0.01 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |  1000 |              1 |   1,880.296 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |  1000 |              1 |   1,888.283 ns |  1.00 |    0.01 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |  1000 |              1 |     919.183 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |  1000 |              1 |     917.314 ns |  1.00 |    0.00 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main |  1000 |              5 |   6,538.792 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 |  1000 |              5 |   6,373.152 ns |  0.98 |    0.06 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main |  1000 |              5 |     824.639 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 |  1000 |              5 |     818.418 ns |  0.99 |    0.02 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main | 10000 |              1 |  40,440.282 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 | 10000 |              1 |  40,415.237 ns |  1.00 |    0.02 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main | 10000 |              1 |  23,686.434 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 | 10000 |              1 |  23,855.038 ns |  1.01 |    0.02 |
|                                    |                         |       |                |                |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main | 10000 |              5 | 103,406.399 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546 | 10000 |              5 | 103,863.661 ns |  1.00 |    0.02 |
|                                    |                         |       |                |                |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main | 10000 |              5 |  19,293.574 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546 | 10000 |              5 |  19,548.697 ns |  1.01 |    0.01 |

Perf_SingleCharFrozenDictionary

Optimization no longer applies. No filter but also no regression!
There seems to be a slight regression for count = 1000 but I would guess this is just noise otherwise it makes no sense.

|                             Method |                Toolchain | Count |         Mean | Ratio | RatioSD |
|----------------------------------- |------------------------- |------ |-------------:|------:|--------:|
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ |    10 |     24.76 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ |    10 |     24.57 ns |  0.99 |    0.01 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ |    10 |     20.67 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ |    10 |     20.78 ns |  1.00 |    0.06 |
|                                    |                          |       |              |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ |   100 |    231.95 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ |   100 |    231.42 ns |  1.00 |    0.02 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ |   100 |    185.49 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ |   100 |    186.87 ns |  1.01 |    0.01 |
|                                    |                          |       |              |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ |  1000 |  2,321.35 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ |  1000 |  2,314.96 ns |  1.00 |    0.02 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ |  1000 |  1,877.74 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ |  1000 |  1,925.35 ns |  1.03 |    0.02 |
|                                    |                          |       |              |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ | 10000 | 23,349.12 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ | 10000 | 23,135.64 ns |  0.99 |    0.02 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ | 10000 | 18,893.20 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ | 10000 | 18,802.59 ns |  1.00 |    0.02 |

Perf_SubstringFrozenDictionary

Same as Perf_SingleCharFrozenDictionary

|                             Method |                Toolchain | Count |         Mean | Ratio | RatioSD |
|----------------------------------- |------------------------- |------ |-------------:|------:|--------:|
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ |    10 |     38.35 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ |    10 |     37.61 ns |  0.98 |    0.02 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ |    10 |     34.30 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ |    10 |     34.47 ns |  1.01 |    0.01 |
|                                    |                          |       |              |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ |   100 |    380.56 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ |   100 |    376.95 ns |  0.99 |    0.02 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ |   100 |    336.54 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ |   100 |    339.07 ns |  1.01 |    0.01 |
|                                    |                          |       |              |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ |  1000 |  4,116.22 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ |  1000 |  4,095.17 ns |  0.99 |    0.01 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ |  1000 |  3,661.51 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ |  1000 |  3,638.98 ns |  0.99 |    0.01 |
|                                    |                          |       |              |       |         |
|  TryGetValue_True_FrozenDictionary |    \dotnet-runtime-main\ | 10000 | 61,387.11 ns |  1.00 |    0.00 |
|  TryGetValue_True_FrozenDictionary | \dotnet-runtime-pr92546\ | 10000 | 60,137.52 ns |  0.98 |    0.05 |
|                                    |                          |       |              |       |         |
| TryGetValue_False_FrozenDictionary |    \dotnet-runtime-main\ | 10000 | 47,288.25 ns |  1.00 |    0.00 |
| TryGetValue_False_FrozenDictionary | \dotnet-runtime-pr92546\ | 10000 | 46,997.14 ns |  1.00 |    0.03 |

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM, big thanks for your contribution @andrewjsaid !

@adamsitnik adamsitnik added this to the 9.0.0 milestone Dec 11, 2023
@adamsitnik adamsitnik merged commit c28bec4 into dotnet:main Dec 11, 2023
101 of 106 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Jan 11, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Collections community-contribution Indicates that the PR has been added by a community member tenet-performance Performance related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants