Windows registry performance in multithreading

The Windows registry (WR) is a NoSQL DBMS based on a semi-structured hierarchical data model. Unlike configuration files approach, WR:

  • can manage millions of keys and values
  • is thread-safe
  • is an in-memory database
  • supports some data types
  • supports a secure access to keys and values
  • has GUI (regedit) and command line (reg) tools to manage data including querying, import/export and backup/restore

What is a WR performance on relatively big (hundreds of thousand and millions keys/values) data volume?

Scenario

The test data structure is following

Factory
|-- Workshops
    |--Workshop1 (code, title, workplace count)
    |  |-- Employees
    |      |-- Employee1 (first and last names, post, sex, age, wage)
    |      ...
    |      |-- EmployeeM
    |-- WorkshopN
       |-- Employees
           |-- Employee1
           ...
           |-- EmployeeM

The test program:

  • insert test data in HKEY_CURRENT_USER database in a single thread mode
  • delete data
  • insert test data in a multiple thread mode (selected number of threads is equals the logical CPU core count)

Let's take N = 1000 and M = 1000 so the total count of keys is 1 million and total count of values is (3 * 1000) + (6 * 1000000) = 6003000. I guess any XML or JSON config database file having same key count may not be simply open by the program.

Test program

The test program is compiled with Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27025.1 (Visual Studio 2017 Pro).

#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
#include <random>
#include <vector>
#include <thread>
#include <ctime>
#include <chrono>
#include <windows.h>
 
using namespace std;
using namespace std::chrono;
 
const int Workshop_MaxCount = 1000;
const int Employee_MaxCount = 1000;
const int TotalKeyCount = Workshop_MaxCount * Employee_MaxCount;
const int ThreadsInBatch = 8;
const wstring DB_BasePath = L"TestDB\\Factory";
 
class Timer
{
public:
	Timer() {}
	void Start()
	{
		t1 = system_clock::now();
	}
	void Stop()
	{
		t2 = system_clock::now();
	}
	long long GetDurationMsec()
	{
		return duration_cast<std::chrono::milliseconds>(t2 - t1).count();
	}
private:
	system_clock::time_point t1 = system_clock::now();
	system_clock::time_point t2;
};
 
class RndHelper
{
public:
	RndHelper()
	{
		default_random_engine rnd_engine(m_device());
		m_engine = std::move(rnd_engine);
	}
	int RandomRange(int from, int to)
	{
		uniform_int_distribution<int> dist(from, to);
		return dist(m_engine);
	}
	bool RandomBool()
	{
		std::mt19937 gen(m_device());
		uniform_real_distribution<double> dist(0, 1); // [0, 1)
		return (dist(gen) < 0.5);
	}
private:
	random_device m_device;
	default_random_engine m_engine;
};
 
 
bool DeleteDatabase()
{
	HKEY key;
	if (RegOpenKey(HKEY_CURRENT_USER, DB_BasePath.c_str(), &key) == ERROR_SUCCESS)
	{
		wcout << L"Deleting existing test database (" << TotalKeyCount << L" keys)... ";
		RegCloseKey(key);
		LSTATUS res = RegDeleteTree(HKEY_CURRENT_USER, DB_BasePath.c_str());
		if (res != ERROR_SUCCESS)
			cout << L"error. Code: " << res << endl;
		else
		{
			wcout << L"done" << endl;
			if ((res = RegCreateKey(HKEY_CURRENT_USER, DB_BasePath.c_str(), &key)) != ERROR_SUCCESS)
			{
				wcout << L"Error creating database. Code: " << res << endl;
				return false;
			}
			RegCloseKey(key);
			return true;
		}
		return false;
	}
	return true;
}
 
void InsertEmployees(const wstring parent_keyname, const int max_count)
{
	RndHelper rnd;
	for (int j = 0; j < max_count; j++)
	{
		wstringstream name;
		name << parent_keyname << L"\\Employees\\Employee" << setw(4) << setfill(L'0') << j;
		HKEY key;
		if (RegCreateKey(HKEY_CURRENT_USER, name.str().c_str(), &key) == ERROR_SUCCESS)
		{
			wstring s = L"FN" + std::to_wstring(rnd.RandomRange(100000, 999999));
			RegSetKeyValue(key, NULL, L"FirstName", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
			s = L"LN" + std::to_wstring(rnd.RandomRange(100000, 999999));
			RegSetKeyValue(key, NULL, L"LastName", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
			s = L"P" + std::to_wstring(rnd.RandomRange(0, 9));
			RegSetKeyValue(key, NULL, L"Post", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
			s = rnd.RandomBool() ? L"F" : L"M";
			RegSetKeyValue(key, NULL, L"Sex", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
			s = std::to_wstring(rnd.RandomRange(18, 67));
			RegSetKeyValue(key, NULL, L"Age", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
			DWORD dw3 = static_cast<DWORD>(rnd.RandomRange(1500, 3000));
			RegSetKeyValue(key, NULL, L"Wage", REG_DWORD, &dw3, sizeof(dw3));
			RegCloseKey(key);
		}
	}
}
 
void InsertWorksop(int num)
{
	RndHelper rnd;
	wstringstream name;
	name << DB_BasePath << L"\\Workshops\\Workshop" << setw(4) << setfill(L'0') << num;
	HKEY key;
	if (RegCreateKey(HKEY_CURRENT_USER, name.str().c_str(), &key) == ERROR_SUCCESS)
	{
		wstring s = L"C" + std::to_wstring(num + 1); // std::to_wstring(val); // static_cast<int>(rand()));
		RegSetKeyValue(key, NULL, L"Code", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
		s = L"Name" + std::to_wstring(rnd.RandomRange(1000000, 9999999));
		RegSetKeyValue(key, NULL, L"Name", REG_SZ, s.data(), s.size() * sizeof(wchar_t));
		DWORD dw = static_cast<DWORD>(rnd.RandomRange(0, 100));
		RegSetKeyValue(key, NULL, L"WorkplaceCount", REG_DWORD, &dw, sizeof(dw));
		RegCloseKey(key);
		InsertEmployees(name.str(), Employee_MaxCount);
	}
}
 
void TestInSingleThread()
{
	wcout << L"Test in single thread" << endl;
	wcout << L"Inserting data..." << endl;
	for (int i = 0; i < Workshop_MaxCount; i++)
	{
		InsertWorksop(i);
		if (i > 0 && i % 100 == 0)
			wcout << L"Workshop added: " << i << endl;
	}
	wcout << L"Finished" << endl;
}
 
void TestInMultithread()
{
	wcout << L"Test in multiple threads" << endl;
	vector<thread> threads;
	for (int i = 0; i < Workshop_MaxCount; i++)
	{
		threads.push_back(thread(InsertWorksop, i));
		if (i % ThreadsInBatch == 0)
		{
			for (thread& th : threads)
				th.join();
			threads.clear();
		}
		if (i > 0 && i % 100 == 0)
			wcout << L"Workshop added: " << i << endl;
	}
	for (thread& th : threads)
		th.join();
	wcout << L"Finished" << endl;
}
 
int main()
{
	if (!DeleteDatabase())
		return 1;
	Timer t1;
	t1.Start();
	TestInSingleThread();
	t1.Stop();
	wcout << L"Elapsed time (single tread), sec: " << t1.GetDurationMsec() / 1000 << endl << endl;
	t1.Start();
	if (!DeleteDatabase())
		return 1;
	t1.Stop();
	int keyCount = Workshop_MaxCount * Employee_MaxCount;
	wcout << L"Elapsed time, sec: " << t1.GetDurationMsec() / 1000 << endl << endl;
	t1.Start();
	TestInMultithread();
	t1.Stop();
	wcout << L"Elapsed time (multiple treads), sec: " << t1.GetDurationMsec() / 1000 << endl << endl;
	if (!DeleteDatabase())
		return 1;
	return 0;
}

Results

Ideally, the test should be run in pure console mode. If you run it from Windows GUI check that all updates are installed and they are not running, close all applications to minimize registry lock collisions.

Tests are performed on the development PC:

  • Intel i7-7820HQ, 4 cores at 2.9 GHz, hyper-threading is on
  • 16 GB of RAM
  • SSD
  • Windows 10 Pro

Test in single thread
Inserting data...
Workshop added: 100
Workshop added: 200
Workshop added: 300
Workshop added: 400
Workshop added: 500
Workshop added: 600
Workshop added: 700
Workshop added: 800
Workshop added: 900
Finished
Elapsed time (single tread), sec: 206
 
Deleting existing test database (1000000 keys)... done
Elapsed time, sec: 268
 
Test in multiple threads
Workshop added: 100
Workshop added: 200
Workshop added: 300
Workshop added: 400
Workshop added: 500
Workshop added: 600
Workshop added: 700
Workshop added: 800
Workshop added: 900
Finished
Elapsed time (multiple treads), sec: 94
 
Deleting existing test database (1000000 keys)... done

As you can see, the multi-threaded inserting of data is more that twice faster.

Conclusions

Windows registry is the standard Windows-compliant database storage for application local data. Don't hesitate use it to avoid big configuration files etc.