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.
- Bidirectional Mapping: Fast lookups from Left -> Right and Right -> Left.
- Thread-Safe: Fully thread-safe implementation using
ReaderWriterLockSlimfor high-concurrency read scenarios. - Atomic Operations:
Add,Remove, andClearoperations 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.
using BiMap;
// Create a new BiMap instance
var countryCodes = new BiMap<int, string>();// 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// 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 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 indexThis 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}");
}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 plainlockand 1.6 ns with no synchronization at all.ReaderWriterLockSlimcosts more per call thanlockwhen nobody else is around. - Under concurrent reads it pays off. With 8 threads reading at once,
BiMapfinishes 100,000 lookups 3.4× faster than thelockversion (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
FrozenDictionaryis built for exactly that case).BiMapis for maps that change while other threads read them.
Run them yourself:
dotnet run -c Release --project benchmarks/BiMap.Benchmarks -- --filter '*'This project is licensed under the MIT License.