I'm sure you've heard the phrase "It's an implementation detail". Details matter when it's time to scale and milliseconds add up quickly. Understanding the relevant details of the .NET subsystems can make the difference between scaling and failing.
.NET team. • Co-creator of NuGet, SignalR, ASP.NET Core and the Azure SignalR Service. • I spend lots of my time helping customers debug and diagnose complex issues with .NET in production. • I spend the remainder of time in meetings and writing code.
modern, high performance platform. • I’ve been spending a chunk of my time helping teams at Microsoft design high performance libraries and diagnosing problems in large .NET based services. • There’s a lot of FUD surrounding premature optimization that I’d like to help dispel.
humans with specific assumptions and scenarios in mind. • Understanding those assumptions can make a big difference at scale. • Understanding the relevant details can help you root cause problems quickly.
be a handful of these instances in practice.” • “This should only happen once at the start of the application, so it can be expensive.” • “Nobody is going to create this per request.” Software Engineer
mean the number of users, the amount of input (like the size of data), or number of times that data needs to be processed (e.g number of requests). Scaling as an engineer means knowing what to ignore and knowing what to pay close attention to.
and learn as much as you can about any area you are working in. • There will be a point of diminishing returns. • Building up a mental model for how things work can be very effective. • Figure out the “cliff notes” of an area. TL;DR
Reading code others wrote. • Talking to subject matter experts. • Reading the documentation, including the fine print. • Debug the code. • Stack Overflow answers • Read the source code again.
optimizes for throughput, not reduced memory usage. • Timers are optimized for creation and deletion. The assumption is they don’t fire most of the time. • Configuration is expected to be built once on application start.
context, we’re going to use real examples. • Various teams at Microsoft use .NET extensively for large scale services. • We’ll look at some problems and root causes. • These code samples are in the hot path.
new GZipStream(new MemoryStream(raw), CompressionMode.Decompress)) { const int size = 4096; byte[] buffer = new byte[size]; using (var memory = new MemoryStream()) { int count; do { count = stream.Read(buffer, 0, size); if (count > 0) { memory.Write(buffer, 0, count); } } while (count > 0); return memory.ToArray(); } } } input buffer Copy buffer Another internal buffer Creates a new buffer Copy into internal buffer Allocates an 8K buffer internally
common in server applications. • Lots of large temporary buffers that are thrown away per operation. • This can wreak havoc on the GC and your application as a result. • Be mindful about excessive buffer allocations and copies. • Use Streams/Pipelines for large data sets. • Pool and re-use buffers when you need to operate on in-memory data.
the strings • If the string is already interned, then the check is cheap and lock free • Strings that aren’t interned pay the cost of the global lock • If you need to intern strings, consider implementing your own cache or using a nuget package that does it.
var visitedActivityIds = new HashSet<Guid>(); var unvisitedOperations = new Queue<Operation>(); unvisitedOperations.Enqueue(root); while (unvisitedOperations.Any()) { var currentOperation = unvisitedOperations.Dequeue(); if (visitedActivityIds.Contains(currentOperation.ActivityId)) { continue; } visitedActivityIds.Add(currentOperation.ActivityId); if (predicate.Invoke(currentOperation)) { yield return currentOperation; } currentOperation.ChildActions.Where(r => r.IsComplete).ToList().ForEach(unvisitedOperations.Enqueue); } } LINQ LINQ List allocation Delegate allocation
var visitedActivityIds = new HashSet<Guid>(); var unvisitedOperations = new Queue<Operation>(); unvisitedOperations.Enqueue(root); while (unvisitedOperations.Count > 0) { var currentOperation = unvisitedOperations.Dequeue(); if (!visitedActivityIds.Add(currentOperation.ActivityId)) { continue; } if (predicate.Invoke(currentOperation)) { yield return currentOperation; } foreach (var item in currentOperation.ChildActions) { if (item.IsComplete) { unvisitedOperations.Enqueue(item); } } } } Directly use Count Add replaces the Contains call Use a normal foreach loop
and expressive. • It should not be used in your application’s hot paths. • When you need to reduce allocations or save CPU cycles, LINQ is an easy target. • Optimized for what it does • There are lots of special cases to avoid expensive calls.
code. • It generates an IL specific to the regular expression specified. • Every new call will create new methods that needs to be JITTed. • Cache these aggressively.
CheckIfFilePathIsValid(string localPath) { string potentialId = validPath.Match(localPath).Groups["id"].Value; if (!Guid.TryParse(potentialId, out var unused)) { return false; } string localPathRoot = Path.GetPathRoot(localPath); string systemLocalPathRoot = Path.GetPathRoot(Environment.SystemDirectory); return localPathRoot.Equals(systemLocalPathRoot, StringComparison.OrdinalIgnoreCase); } Compile once and cache Check for Guid first Remove ToLower calls and do case insensitive comparison
= cache._options.Clock.UtcNow; foreach (CacheEntry entry in cache._entries.Values) { if (entry.CheckExpired(now)) { cache.RemoveEntry(entry); } } } This is a ConcurrentDictionary
• Reads are lock free. • Writes are not! • Several APIs lock the entire collection (.Keys, .Values etc). • Some APIs take snapshots of the underlying collections (allocations and copies). • The default number of concurrent writes is equal to the number of processors on the machine (you’ll have that many locks by default).
your intuition. • Think about how things are implemented. • Quick to call != Quick to execute. • Everything is a tradeoff. • The compiler isn’t nearly as smart as you think it is…
on a learning journey. • Learn from the code you’ve written in the past. • Nobody is perfect. • These examples came from Microsoft. • I learned some things making these slides.
Make sure your knowledge is up to date! • Specific details often change, concepts don’t change as often. • Continuous measurement is important. Things tend to improve over time.
of the system. • Figure out which parts need to scale. • What’s on the hot path? • Figure out how those parts work, down to the relevant details. • Optimize your code appropriately.