It was some time ago when a user of our VSdocman reported about 20 second delay when he invoked some of its functions. It wasn't that easy to fix it because we couldn't reproduce the issue on our side. Finally we have found the reason, created a workaround in our code and forgot it.
Recently, I've encountered a similar problem and it took some time to recall that we have already faced it. So I'm putting the info here for my own reference and for anyone else.
The problem is as follows. We often read Windows registry to obtain some information, e.g. GAC-ed assemblies info. We didn't pay much attention to the performance as it always worked fast. Basically, we needed to read names of all subkeys of a specific key. That's all. Even if we created thousands of tmp subkeys just for our tests, the retrieval was fast, a few milliseconds at most.
But the user had much longer delays and our trace logs shown he was right. It turned out that a single call to RegistryKey.GetSubKeyNames() took about 17 seconds. As I wrote, we couldn't get more than a half second even with our craziest tests. Then after closer investigation we have found a test case with the same results. The problem wasn't caused by large number of subkeys but by the length of their names.
For our example we will work with HKEY_CLASSES_ROOT\SOFTWARE\Helixoft_RegTest registry key. The HKEY_CLASSES_ROOT hive was chosen because our real life implementation was accessing this hive too. We'll write 5000 subkeys in it and they will be named in the form:
C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E123456|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer0001.dll C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E123456|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer0002.dll etc.
Then reading these subkeys with RegistryKey.GetSubKeyNames() takes about 16 ms. That's fine. The name length of subkey names is 133 characters. If you try shorter names, there's no difference.
Now let's do exactly the same but add one character to the names, so that they be 134 characters long:
C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E1234567|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer0001.dll C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E1234567|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer0002.dll etc.
When you read them now, it takes 27 seconds! If you try longer names, the time doesn't increase. It seems there's a threshold of 133/134 characters in subkey names which causes the significant performance issue.
You can try it yourself. Create a C# console application with the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Win32;
using System.Globalization;
using System.Text.RegularExpressions;
namespace RegistrySpeedTester
{
class Program
{
//const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|ExtensionSDKs|MSTestFramework|11.0|References|CommonConfiguration|neutral|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0}.dll";
//const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|ExtensionSDKs|M|neutral|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0}.dll"; //14s
//const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0}.dll"; // 0.0156s
//const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E12345678|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0:0000}.dll";//14s 179chars
//const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E1234|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0:0000}.dll"; // 0.0156s ; total chars=175 ; key name chars=131
//const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E123456|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0:0000}.dll"; // 0.0312s ; total chars=177 ; key name chars=133
const string SUB_KEY_NAME = @"C:|Program Files (x86)|Microsoft SDKs|Windows|v8.1|E1234567|Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer{0:0000}.dll"; // 14.45s ; total chars=178 ; key name chars=134
const string PARENT_KEY_PATH = @"SOFTWARE\Helixoft_RegTest";
const int SUB_KEYS_COUNT = 5000;
private enum Operations
{
read,
create,
both
}
/// <summary>
/// Main method.
/// </summary>
/// <param name="args"><para>Command line arguments:</para>
/// <list type="bullet">
/// <item>
/// <term>\read or No arguments</term>
/// <description>Read the registry and then delete the sub-keys.</description>
/// </item>
/// <item>
/// <term>\create</term>
/// <description>Create the registry sub-keys and exit.</description>
/// </item>
/// <item>
/// <term>\both</term>
/// <description>Create the registry sub-keys, read them, delete them and then
/// exit.</description>
/// </item>
/// </list>
/// <para></para></param>
static void Main(string[] args)
{
Operations operation = Operations.read;
if (args.Length > 0)
{
switch (args[0].ToLower())
{
case @"\create":
operation = Operations.create;
break;
case @"\both":
operation = Operations.both;
break;
}
}
//RegistryKey regHive = Registry.CurrentUser;
RegistryKey regHive = Registry.ClassesRoot;
//RegistryKey regHive = Registry.LocalMachine;
if (operation == Operations.create || operation == Operations.both)
{
CreateSubKeys(regHive, SUB_KEYS_COUNT);
}
if (operation == Operations.read || operation == Operations.both)
{
ReadSubKeys(regHive);
}
Console.Write("Press any key to exit");
Console.ReadKey(true);
// clean up the registry
if (operation == Operations.read || operation == Operations.both)
{
regHive.DeleteSubKeyTree(PARENT_KEY_PATH, false);
}
}
private static void CreateSubKeys(RegistryKey regHive, int count)
{
// create sub-keys
regHive.DeleteSubKeyTree(PARENT_KEY_PATH, false); // just for sure
RegistryKey parentKey;
parentKey = regHive.CreateSubKey(PARENT_KEY_PATH);
for (int i = 0; i < count; i++)
{
string subKeyName;
subKeyName = string.Format(SUB_KEY_NAME, i);
RegistryKey subKey = parentKey.CreateSubKey(subKeyName);
subKey.Close();
}
parentKey.Close();
}
private static void ReadSubKeys(RegistryKey regHive)
{
// retrieve sub-key names
RegistryKey parentKey;
parentKey = regHive.OpenSubKey(PARENT_KEY_PATH);
string[] subKeyNames = new string[] { };
DateTime start = DateTime.Now;
Console.WriteLine(String.Format("Started at: {0}", GetTimestampString(start)));
if (parentKey != null)
{
subKeyNames = parentKey.GetSubKeyNames();
}
DateTime end = DateTime.Now;
Console.WriteLine(String.Format("Finished at: {0}", GetTimestampString(end)));
Console.WriteLine(String.Format("There were {0} sub key names retrieved in {1}.", subKeyNames.Length, end.Subtract(start).ToString()));
string longestName = "";
foreach (string subKeyName in subKeyNames)
{
if (subKeyName.Length > longestName.Length)
{
longestName = subKeyName;
}
}
Console.WriteLine(String.Format("The longest key name was {0}\nKey length={1}\nTotal key length={2}",
parentKey.Name + "\\" + longestName,
longestName.Length,
parentKey.Name.Length + longestName.Length + 1));
}
/// <summary>
/// Gets a timestamp info with milliseconds.
/// </summary>
private static string GetTimestampString(DateTime timestamp)
{
// Append millisecond pattern to current culture's full date time pattern
string pattern = DateTimeFormatInfo.CurrentInfo.LongTimePattern;
pattern = Regex.Replace(pattern, "(:ss|:s)", "$1.fff");
return timestamp.ToShortDateString() + " " + timestamp.ToString(pattern);
}
}
}
Then call the exe with \both switch. It will create 5000 registry keys, read their names, delete the keys and report the time needed for reading the names. You can comment out the SUB_KEY_NAME constant and try another one that is commented out.
Some conclusions
I tried the tests on 2 different machines with the same results.
I tried to circumvent the RegistryKey.GetSubKeyNames() by duplicating its functionality from the .NET source code. This method uses pinvoke to RegEnumKeyEx function internally. I placed a stopwatch around the call to this function and it confirmed that all the time was consumed by RegEnumKeyEx. So there's no way we could make it faster from .NET.
I tried the same test with another registry hive instead of HKEY_CLASSES_ROOT. With HKEY_LOCAL_MACHINE all the test were fast!
So this behavior is quite random. It's not a bug but it's worth to be aware of it, especially if you diagnose registry performance.
Update 11 March 2016
Well, I think I have solved the mystery. I completely forgot that HKEY_CLASSES_ROOT is just a virtual merged view. According to this MSDN article:
- The merged view includes all subkeys of the HKEY_CURRENT_USER\Software\Classes key.
- The merged view includes all immediate subkeys of the HKEY_LOCAL_MACHINE\Software\Classes key that do not duplicate the subkeys of HKEY_CURRENT_USER\Software\Classes.
And it turned out that this merging process takes so long. I proved it when I extended the example from above.
If you write keys to a key under HKEY_CLASSES_ROOT, the system stores the information under HKEY_LOCAL_MACHINE\Software\Classes. If you write values to a key under HKEY_CLASSES_ROOT, and the key already exists under HKEY_CURRENT_USER\Software\Classes, the system will store the information there instead of under HKEY_LOCAL_MACHINE\Software\Classes.
This means that the keys written in my example are in fact written to HKEY_LOCAL_MACHINE\Software\Classes. So I extended the code in the following way. After I read the key names from HKEY_CLASSES_ROOT (which takes 27 seconds) I immediately read them also directly from HKEY_LOCAL_MACHINE\Software\Classes. And bingo, it took only 16 ms.
The final conclusion is that reading HKEY_CLASSES_ROOT involves merging of the keys, and this may be very slow. If you experience the performance issues, it's worth to consider reading directly from HKEY_LOCAL_MACHINE\Software\Classes and HKEY_CURRENT_USER\Software\Classes and merge the keys manually.