Skip to content

Commit 8454731

Browse files
committed
Issue #29, Phase 4 - Resolver Lookups
1 parent a7e804c commit 8454731

8 files changed

Lines changed: 560 additions & 70 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
```
2+
3+
BenchmarkDotNet v0.14.0, macOS 26.1 (25B78) [Darwin 25.1.0]
4+
Apple M1 Max, 1 CPU, 10 logical and 10 physical cores
5+
.NET SDK 8.0.104
6+
[Host] : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
7+
Job-RDUOPE : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
8+
9+
IterationCount=5 WarmupCount=2
10+
11+
```
12+
| Method | Mean | Error | StdDev | Gen0 | Allocated |
13+
|-------------------------------------- |----------:|---------:|---------:|-------:|----------:|
14+
| 'Zone Lookup: Dictionary<string>' | 102.18 ns | 1.732 ns | 0.268 ns | 0.0255 | 160 B |
15+
| 'Zone Lookup: Dictionary<struct>' | 44.19 ns | 0.750 ns | 0.116 ns | - | - |
16+
| 'Zone Lookup: ConcurrentDict<string>' | 102.22 ns | 1.065 ns | 0.276 ns | 0.0255 | 160 B |
17+
| 'Zone Lookup: ConcurrentDict<struct>' | 41.82 ns | 0.905 ns | 0.235 ns | - | - |
18+
| 'Zone Lookup: FrozenDict<struct>' | 44.88 ns | 5.114 ns | 1.328 ns | - | - |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Allocated
2+
'Zone Lookup: Dictionary<string>',Job-RDUOPE,False,Default,Default,Default,Default,Default,Default,0000000000,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,5,Default,Default,Default,Default,Default,Default,Default,Default,16,2,102.18 ns,1.732 ns,0.268 ns,0.0255,160 B
3+
'Zone Lookup: Dictionary<struct>',Job-RDUOPE,False,Default,Default,Default,Default,Default,Default,0000000000,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,5,Default,Default,Default,Default,Default,Default,Default,Default,16,2,44.19 ns,0.750 ns,0.116 ns,0.0000,0 B
4+
'Zone Lookup: ConcurrentDict<string>',Job-RDUOPE,False,Default,Default,Default,Default,Default,Default,0000000000,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,5,Default,Default,Default,Default,Default,Default,Default,Default,16,2,102.22 ns,1.065 ns,0.276 ns,0.0255,160 B
5+
'Zone Lookup: ConcurrentDict<struct>',Job-RDUOPE,False,Default,Default,Default,Default,Default,Default,0000000000,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,5,Default,Default,Default,Default,Default,Default,Default,Default,16,2,41.82 ns,0.905 ns,0.235 ns,0.0000,0 B
6+
'Zone Lookup: FrozenDict<struct>',Job-RDUOPE,False,Default,Default,Default,Default,Default,Default,0000000000,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,5,Default,Default,Default,Default,Default,Default,Default,Default,16,2,44.88 ns,5.114 ns,1.328 ns,0.0000,0 B
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html lang='en'>
3+
<head>
4+
<meta charset='utf-8' />
5+
<title>DnsBench.CacheLookupBenchmarks-20251126-010234</title>
6+
7+
<style type="text/css">
8+
table { border-collapse: collapse; display: block; width: 100%; overflow: auto; }
9+
td, th { padding: 6px 13px; border: 1px solid #ddd; text-align: right; }
10+
tr { background-color: #fff; border-top: 1px solid #ccc; }
11+
tr:nth-child(even) { background: #f8f8f8; }
12+
</style>
13+
</head>
14+
<body>
15+
<pre><code>
16+
BenchmarkDotNet v0.14.0, macOS 26.1 (25B78) [Darwin 25.1.0]
17+
Apple M1 Max, 1 CPU, 10 logical and 10 physical cores
18+
.NET SDK 8.0.104
19+
[Host] : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
20+
Job-RDUOPE : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
21+
</code></pre>
22+
<pre><code>IterationCount=5 WarmupCount=2
23+
</code></pre>
24+
25+
<table>
26+
<thead><tr><th>Method </th><th>Mean</th><th>Error</th><th>StdDev</th><th>Gen0</th><th>Allocated</th>
27+
</tr>
28+
</thead><tbody><tr><td>&#39;Zone Lookup: Dictionary&lt;string&gt;&#39;</td><td>102.18 ns</td><td>1.732 ns</td><td>0.268 ns</td><td>0.0255</td><td>160 B</td>
29+
</tr><tr><td>&#39;Zone Lookup: Dictionary&lt;struct&gt;&#39;</td><td>44.19 ns</td><td>0.750 ns</td><td>0.116 ns</td><td>-</td><td>-</td>
30+
</tr><tr><td>&#39;Zone Lookup: ConcurrentDict&lt;string&gt;&#39;</td><td>102.22 ns</td><td>1.065 ns</td><td>0.276 ns</td><td>0.0255</td><td>160 B</td>
31+
</tr><tr><td>&#39;Zone Lookup: ConcurrentDict&lt;struct&gt;&#39;</td><td>41.82 ns</td><td>0.905 ns</td><td>0.235 ns</td><td>-</td><td>-</td>
32+
</tr><tr><td>&#39;Zone Lookup: FrozenDict&lt;struct&gt;&#39;</td><td>44.88 ns</td><td>5.114 ns</td><td>1.328 ns</td><td>-</td><td>-</td>
33+
</tr></tbody></table>
34+
</body>
35+
</html>

Dns/DnsLookupKey.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// //-------------------------------------------------------------------------------------------------
2+
// // <copyright file="DnsLookupKey.cs" company="stephbu">
3+
// // Copyright (c) Steve Butler. All rights reserved.
4+
// // </copyright>
5+
// //-------------------------------------------------------------------------------------------------
6+
7+
namespace Dns
8+
{
9+
using System;
10+
11+
/// <summary>
12+
/// Value-type key for DNS request/response tracking.
13+
/// Eliminates string allocation (~80 bytes) per lookup by using struct with precomputed hash.
14+
/// </summary>
15+
/// <remarks>
16+
/// Phase 4 optimization: replaces string.Format("{id}|{class}|{type}|{name}") with zero-allocation struct.
17+
/// Used in DnsServer._requestResponseMap to correlate forwarded queries with their originators.
18+
/// </remarks>
19+
public readonly struct DnsRequestKey : IEquatable<DnsRequestKey>
20+
{
21+
public readonly ushort QueryId;
22+
public readonly ResourceClass Class;
23+
public readonly ResourceType Type;
24+
public readonly string Name;
25+
private readonly int _hashCode;
26+
27+
public DnsRequestKey(ushort queryId, ResourceClass resClass, ResourceType resType, string name)
28+
{
29+
QueryId = queryId;
30+
Class = resClass;
31+
Type = resType;
32+
Name = name ?? string.Empty;
33+
// Precompute hash for fast dictionary lookups
34+
_hashCode = HashCode.Combine(QueryId, Class, Type, StringComparer.OrdinalIgnoreCase.GetHashCode(Name));
35+
}
36+
37+
public DnsRequestKey(DnsMessage message)
38+
: this(
39+
message.QueryIdentifier,
40+
message.QuestionCount > 0 ? message.Questions[0].Class : ResourceClass.IN,
41+
message.QuestionCount > 0 ? message.Questions[0].Type : ResourceType.A,
42+
message.QuestionCount > 0 ? message.Questions[0].Name : string.Empty)
43+
{
44+
}
45+
46+
public bool Equals(DnsRequestKey other)
47+
{
48+
return QueryId == other.QueryId
49+
&& Class == other.Class
50+
&& Type == other.Type
51+
&& string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
52+
}
53+
54+
public override bool Equals(object obj) => obj is DnsRequestKey other && Equals(other);
55+
56+
public override int GetHashCode() => _hashCode;
57+
58+
public static bool operator ==(DnsRequestKey left, DnsRequestKey right) => left.Equals(right);
59+
public static bool operator !=(DnsRequestKey left, DnsRequestKey right) => !left.Equals(right);
60+
61+
public override string ToString() => $"{QueryId}|{Class}|{Type}|{Name}";
62+
}
63+
64+
/// <summary>
65+
/// Value-type key for DNS zone lookups.
66+
/// Eliminates string allocation for zone map key generation.
67+
/// </summary>
68+
/// <remarks>
69+
/// Phase 4 optimization: replaces string.Format("{host}|{class}|{type}") with zero-allocation struct.
70+
/// Used in SmartZoneResolver._zoneMap for hostname resolution.
71+
/// </remarks>
72+
public readonly struct DnsZoneLookupKey : IEquatable<DnsZoneLookupKey>
73+
{
74+
public readonly ResourceClass Class;
75+
public readonly ResourceType Type;
76+
public readonly string Host;
77+
private readonly int _hashCode;
78+
79+
public DnsZoneLookupKey(string host, ResourceClass resClass, ResourceType resType)
80+
{
81+
Host = host ?? string.Empty;
82+
Class = resClass;
83+
Type = resType;
84+
// Precompute hash for fast dictionary lookups (case-insensitive for DNS)
85+
_hashCode = HashCode.Combine(
86+
StringComparer.OrdinalIgnoreCase.GetHashCode(Host),
87+
Class,
88+
Type);
89+
}
90+
91+
public bool Equals(DnsZoneLookupKey other)
92+
{
93+
return Class == other.Class
94+
&& Type == other.Type
95+
&& string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase);
96+
}
97+
98+
public override bool Equals(object obj) => obj is DnsZoneLookupKey other && Equals(other);
99+
100+
public override int GetHashCode() => _hashCode;
101+
102+
public static bool operator ==(DnsZoneLookupKey left, DnsZoneLookupKey right) => left.Equals(right);
103+
public static bool operator !=(DnsZoneLookupKey left, DnsZoneLookupKey right) => !left.Equals(right);
104+
105+
public override string ToString() => $"{Host}|{Class}|{Type}";
106+
}
107+
}

Dns/DnsServer.cs

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Dns
1010
{
1111
using System;
12+
using System.Collections.Concurrent;
1213
using System.Collections.Generic;
1314
using System.IO;
1415
using System.Linq;
@@ -27,9 +28,11 @@ internal class DnsServer : IHtmlDump
2728
private long _responses;
2829
private long _nacks;
2930

30-
private Dictionary<string, EndPoint> _requestResponseMap = new Dictionary<string, EndPoint>();
31-
32-
private ReaderWriterLockSlim _requestResponseMapLock = new ReaderWriterLockSlim();
31+
/// <summary>
32+
/// Maps forwarded DNS requests to their originating endpoints.
33+
/// Phase 4: Uses struct key (DnsRequestKey) + ConcurrentDictionary for lock-free, allocation-free lookups.
34+
/// </summary>
35+
private readonly ConcurrentDictionary<DnsRequestKey, EndPoint> _requestResponseMap = new ConcurrentDictionary<DnsRequestKey, EndPoint>();
3336

3437
private ushort port;
3538

@@ -124,17 +127,9 @@ private void ProcessUdpRequest(byte[] buffer, int length, EndPoint remoteEndPoin
124127
//
125128
else // Referral to regular DC DNS servers
126129
{
127-
// store current IP address and Query ID.
128-
try
129-
{
130-
string key = GetKeyName(message);
131-
_requestResponseMapLock.EnterWriteLock();
132-
_requestResponseMap.Add(key, remoteEndPoint);
133-
}
134-
finally
135-
{
136-
_requestResponseMapLock.ExitWriteLock();
137-
}
130+
// store current IP address and Query ID using struct key (zero allocation)
131+
var key = new DnsRequestKey(message);
132+
_requestResponseMap.TryAdd(key, remoteEndPoint);
138133
}
139134

140135
using (PooledMemoryStream responseStream = BufferPool.RentMemoryStream())
@@ -159,52 +154,34 @@ private void ProcessUdpRequest(byte[] buffer, int length, EndPoint remoteEndPoin
159154
}
160155
else
161156
{
162-
// message is response to a delegated query
163-
string key = GetKeyName(message);
164-
try
165-
{
166-
_requestResponseMapLock.EnterUpgradeableReadLock();
157+
// message is response to a delegated query - use struct key (zero allocation)
158+
var key = new DnsRequestKey(message);
167159

168-
EndPoint ep;
169-
if (_requestResponseMap.TryGetValue(key, out ep))
160+
// TryRemove atomically checks and removes - no locks needed
161+
if (_requestResponseMap.TryRemove(key, out EndPoint ep))
162+
{
163+
using (PooledMemoryStream responseStream = BufferPool.RentMemoryStream())
170164
{
171-
// first test establishes presence
172-
try
173-
{
174-
_requestResponseMapLock.EnterWriteLock();
175-
// second test within lock means exclusive access
176-
if (_requestResponseMap.TryGetValue(key, out ep))
177-
{
178-
using (PooledMemoryStream responseStream = BufferPool.RentMemoryStream())
179-
{
180-
message.WriteToStream(responseStream);
181-
Interlocked.Increment(ref _responses);
165+
message.WriteToStream(responseStream);
166+
Interlocked.Increment(ref _responses);
182167

183-
Console.WriteLine("{0} answered {1} {2} {3} to {4}", remoteEndPoint.ToString(), message.Questions[0].Name, message.Questions[0].Class, message.Questions[0].Type, ep.ToString());
168+
Console.WriteLine("{0} answered {1} {2} {3} to {4}", remoteEndPoint.ToString(), message.Questions[0].Name, message.Questions[0].Class, message.Questions[0].Type, ep.ToString());
184169

185-
SendUdp(responseStream.GetBuffer(), 0, (int)responseStream.Position, ep);
186-
}
187-
_requestResponseMap.Remove(key);
188-
}
189-
190-
}
191-
finally
192-
{
193-
_requestResponseMapLock.ExitWriteLock();
194-
}
195-
}
196-
else
197-
{
198-
Interlocked.Increment(ref _nacks);
170+
SendUdp(responseStream.GetBuffer(), 0, (int)responseStream.Position, ep);
199171
}
200172
}
201-
finally
173+
else
202174
{
203-
_requestResponseMapLock.ExitUpgradeableReadLock();
175+
Interlocked.Increment(ref _nacks);
204176
}
205177
}
206178
}
207179

180+
/// <summary>
181+
/// Creates a lookup key from a DNS message.
182+
/// Legacy method preserved for compatibility - prefer using DnsRequestKey struct directly.
183+
/// </summary>
184+
[Obsolete("Use new DnsRequestKey(message) for zero-allocation key creation")]
208185
private string GetKeyName(DnsMessage message)
209186
{
210187
if (message.QuestionCount > 0)

0 commit comments

Comments
 (0)