Skip to content
TahaZahitPublic

About

Thread-safe generic bidirectional map (1:1 dictionary) for .NET with O(1) lookups in both directions.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

 

History

36 Commits

Folders and files

Repository files navigation

.NET NuGet NuGet Downloads License: MIT

BiMap

A thread-safe, high-performance, generic bidirectional map for .NET.

BiMap<TLeft, TRight> allows you to map keys to values and values back to keys with O(1) lookups in both directions. It ensures a strict 1-to-1 mapping between the two types.

Features

  • Bidirectional Mapping: Fast lookups from Left -> Right and Right -> Left.
  • Thread-Safe: Fully thread-safe implementation using ReaderWriterLockSlim for high-concurrency read scenarios.
  • Atomic Operations: Add, Remove, and Clear operations are atomic, ensuring both sides of the map remain synchronized.
  • .NET Standard 2.0: Compatible with .NET Framework 4.6.1+, .NET Core 2.0+, and all modern .NET versions.

Usage

Initialization

using BiMap;

// Create a new BiMap instance
var countryCodes = new BiMap<int, string>();

Adding Items

// Add unique pairs
// Note: Both Left and Right values must be unique in the collection.
bool added = countryCodes.Add(1, "US"); 
countryCodes.Add(44, "UK");
countryCodes.Add(90, "TR");

// Duplicate keys (on either side) will return false and not affect the map
bool result = countryCodes.Add(1, "Canada"); // Returns false

Retrieving Values

// Get Right from Left
string country = countryCodes.GetLeftToRight(90); // Returns "TR"
// Or use safe access
if (countryCodes.TryGetLeftToRight(44, out string name))
{
    Console.WriteLine(name); // "UK"
}

// Get Left from Right (Reverse lookup)
int code = countryCodes.GetRightToLeft("US"); // Returns 1
if (countryCodes.TryGetRightToLeft("UK", out int callingCode))
{
    Console.WriteLine(callingCode); // 44
}

Removing Items

Removing an item from one side automatically removes the corresponding mapping from the other side.

// Remove by Left key
countryCodes.RemoveByLeft(1); 
// Now "US" is also removed from the Right->Left index

// Remove by Right key
countryCodes.RemoveByRight("UK");
// Now 44 is also removed from the Left->Right index

Thread Safety

This library uses ReaderWriterLockSlim to strictly manage access:

  • Multiple readers can access the map simultaneously (e.g., Get, Contains, Enumerate).
  • Exclusive writers block all other readers and writers ensures data integrity (e.g., Add, Remove, Clear).
// Enumeration is thread-safe (returns a snapshot of the current state)
foreach (var pair in countryCodes.EnumerateLeftToRight())
{
    Console.WriteLine($"{pair.Key} -> {pair.Value}");
}

Benchmarks

What does thread safety cost? BiMap is compared with the two ways a bidirectional map is usually hand-rolled: a pair of Dictionary instances with no synchronization (the baseline), and the same pair behind a single lock. Each cell is the mean time for a whole batch over N int ↔ string pairs; the ratio is against the unsynchronized pair.

Operation N 2 × Dictionary 2 × Dictionary + lock BiMap
Add 1,000 30.847 μs 33.885 μs (1.10×) 37.360 μs (1.21×)
Add 100,000 4,012.708 μs 4,466.556 μs (1.11×) 4,712.991 μs (1.17×)
Lookup (left → right) 1,000 1.587 μs 4.351 μs (2.74×) 8.715 μs (5.49×)
Lookup (left → right) 100,000 172.754 μs 483.847 μs (2.80×) 873.783 μs (5.06×)
Reverse lookup (right → left) 1,000 4.242 μs 7.875 μs (1.86×) 12.689 μs (2.99×)
Reverse lookup (right → left) 100,000 590.272 μs 1,013.464 μs (1.72×) 1,569.421 μs (2.66×)
Add, then remove all 1,000 40.018 μs 48.185 μs (1.20×) 57.485 μs (1.44×)
Add, then remove all 100,000 5,780.491 μs 6,600.239 μs (1.14×) 7,457.104 μs (1.29×)
Lookup, 8 reader threads 1,000 4.366 μs 54.215 μs (12.42×) 24.415 μs (5.59×)
Lookup, 8 reader threads 100,000 35.881 μs 7,338.058 μs (204.59×) 2,173.098 μs (60.59×)

Measured with BenchmarkDotNet 0.15.8 (Release, default job) on an Apple M3 Max, macOS 26.6.2, .NET 10.0.0 (Arm64 RyuJIT). This is a developer machine, not an isolated lab, so compare the ratios rather than the absolute times. Raw output: benchmarks/results.

What the numbers say:

  • Single-threaded, the lock is not free. One lookup takes about 8.7 ns in BiMap, 4.4 ns behind a plain lock and 1.6 ns with no synchronization at all. ReaderWriterLockSlim costs more per call than lock when nobody else is around.
  • Under concurrent reads it pays off. With 8 threads reading at once, BiMap finishes 100,000 lookups 3.4× faster than the lock version (2.17 ms vs 7.34 ms), because readers do not queue behind each other.
  • Writes cost 17–44% more than the unsynchronized pair. Allocations are the same in all three: the two dictionaries are the whole footprint.
  • If a map is built once and then only read, a plain pair of dictionaries is faster (and FrozenDictionary is built for exactly that case). BiMap is for maps that change while other threads read them.

Run them yourself:

dotnet run -c Release --project benchmarks/BiMap.Benchmarks -- --filter '*'

License

This project is licensed under the MIT License.

About

Thread-safe generic bidirectional map (1:1 dictionary) for .NET with O(1) lookups in both directions.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages