There is a well-worn adage in software engineering that you should first make your code correct, then make it fast. Most of this book has focused strictly on performance, but this chapter is a little bit of an aside into some important topics that, while not strictly related to performance, may help you in your pursuit of high-performance, scalable applications. By undertaking some good practices to ensure the stability and reliability of your code, you free yourself to make more drastic changes for performance’s sake. When problems do occur, you will more easily narrow down the location of the issue.
Heavy performance optimization is going to defy any abstractions you want to impose on your software. As mentioned numerous times in this book, you must understand the APIs you call in order to make intelligent decisions about how to use them, or whether to use them at all.
That is not enough, however. Take threading, for example. While various versions of the .NET Framework have added abstractions on top of threads that make asynchronous programming easier, taking advantage of this fully will require you to understand how these features interact with the underlying OS threads and its scheduling algorithm. The same is true for debugging memory problems. The GC heap is remarkably simple to inspect, but if you have a huge process that loads thousands of types from hundreds of assemblies, you may run into problems outside of the pure managed world, which will require you to understand a process’s full memory layout.
Finally, the hardware is just as important. In the chapter on JIT, I mentioned things like locality of reference—putting bits of code and data that are used together physically near each other in memory so that they can be efficiently included in a processor’s cache. If you are lucky, your code will target a single hardware platform. If not, then you need to understand how they each execute code differently. You may have different memory limits or different cache sizes, or even more substantial differences such as completely different memory models. Hardware also impacts which kinds of multithreading bugs will be surfaced—some platforms will be more forgiving of synchronization sloppiness, and others will not.
All of this must be balanced with the fact that you cannot optimize everything—you can use profiling to zero-in on the areas of code that need the most attention. You also need to expend effort only in proportion to the true need of performance improvement. On the other hand, the more you understand the building blocks you are working with, the less time and effort you will need to spend in costly performance optimization.
There is no reason why you should allow all components to use the full breadth of every Framework and system API. For example, if you have a strict Task
-based processing model, then centralize that functionality and prohibit any other components from accessing anything in the System.Threading
namespace.
These kinds of rules are particularly important for systems with an extension model. You usually want the platform executing all the hard, dangerous code, while the extensions do simple actions in their respective domains.
One way to prevent dangerous patterns or known-bad performance issues is with static code analysis. There are two primary tools you can freely use to accomplish this in .NET: FxCop and Roslyn Code Analyzers.
FxCop is the older tool and operates on compiled DLLs, meaning it can only understand MSIL. .NET Compiler Code Analyzers (you may have heard the early codename “Roslyn Code Analyzers”), on the other hand, have access to the full syntax tree in the compiler itself, and can run during development. I will show examples of both tools, but I recommend .NET Compiler Code Analyzers for all future development.
FxCop is a free static code analysis tool that ships with Visual Studio. It comes with standard rules in categories such as Performance, Globalization, Security, and more, but you can add a library of your own rules. Many of the performance rules we discuss in this book can be represented as FxCop rules, for example:
TryParse
in lieu of Parse
Regex
objects must be static readonly
and created with the RegexOptions.Compiled
flag)Before you start writing rules, keep in mind that FxCop can only analyze IL and metadata. It has no knowledge of C# or any other high-level language. Because of this, you will not be able to enforce static checks that rely on specific language patterns. Writing your own FxCop rules is easy, but there is little to no official documentation, and you will find yourself relying on analyzing the IL of your programs and making extensive use of IntelliSense to poke through the FxCop API. The more you understand IL, the more complicated rules you can develop.
You will first need to install the FxCop SDK, which is trickier than it should be. If you have Visual Studio Professional or better, then it has been included and rebranded Code Analysis in the IDE, but it is still FxCop underneath. On my machine, the relevant files are located in C:Program Files (x86)Microsoft Visual Studio 14.0Team ToolsStatic Analysis ToolsFxCop
.
If you cannot get access to the right version of Visual Studio, there are still a few options. The easiest way to download it is from CodePlex at https://fxcopinstaller.codeplex.com. If that project has disappeared by the time you read this, then try the Windows 7.1 SDK, which appears to have a broken web installer now, but you can get the ISO image at https://www.microsoft.com/download/details.aspx?id=8442. Download it and extract the installer from the archive SetupWinSDKNetFxToolscab1.cab
. There is a file inside that archive that begins with the name WinSDK_FxCopSetup.exe. Extract that file and rename it to FxCopSetup.exe and you are on your way.
In the source code accompanying this book you will find projects related to FxCop. These are in their own solution file to avoid breaking the build for the rest of the sample projects. FxCopRules contains the rules that will be loaded by the FxCop engine and run against some target assembly. FxCopViolator contains a class with a number of violations that the rules will test against. Follow along with these projects as I explain the various components.
Before you can build the rules, you may need to edit the FxCopRules.csproj file to point to the correct SDK path. The current values should resemble:
<PropertyGroup>
<FxCopSdkDir>C:...Microsoft Fxcop 10.0</FxCopSdkDir>
</PropertyGroup>
<ItemGroup>
<Reference Include="$(FxCopSdkDir)FxCopSdk.dll" />
<Reference Include="$(FxCopSdkDir)Microsoft.CCi.dll" />
</ItemGroup>
Update the FxCopSdkDir value to point to the FxCop installation directory (by default in Program Files (x86)
), or wherever you have placed the appropriate DLLs.
Next, you will need to create a Rules.xml file that contains the metadata for each rule. Our first rule will look like this:
<?xml version="1.0" encoding="utf-8" ?>
<Rules FriendlyName="Custom Rules">
<Rule TypeName="DisallowStaticFieldsRule"
Category="Custom.Arbitrary"
CheckId="HP100">
<Name>Static fields are not allowed</Name>
<Description>Static fields are not allowed because...
</Description>
<Url>http://internaldocumentationsite/FxCop/HP100</Url>
<Resolution>Make the static field '{0}' either
readonly or const.
</Resolution>
<MessageLevel Certainty="90">Error</MessageLevel>
<FixCategories>Breaking</FixCategories>
<Email>[email protected]</Email>
<Owner>Ben Watson</Owner>
</Rule>
</Rules>
Note that the TypeName
attribute must match the name of the rule class that we define next. This XML file must be included in the project with the Build Action set to Embedded Resource.
Each rule we define must derive from a class provided by the FxCop SDK and include some common information, such as the location of the XML rules manifest. To make this more convenient, it is a good idea to create a base class for all of your rules that provides this common functionality.
using Microsoft.FxCop.Sdk;
using System.Reflection;
namespace FxCopRules
{
public abstract class BaseCustomRule : BaseIntrospectionRule
{
// The manifest name is the default namespace plus the name
// of the XML rules file, without the extension.
private const string ManifestName = "FxCopRules.Rules";
// The assembly where the rule manifest is
// embedded (the current assembly in our case).
private static readonly Assembly ResourceAssembly =
typeof(BaseCustomRule).Assembly;
protected BaseCustomRule(string ruleName)
:base(ruleName, ManifestName, ResourceAssembly)
{
}
}
}
Next, define a class that derives from BaseCustomRule
that will be for a specific violation you want to check. The first example will disallow all static
fields, but allow const
and readonly
fields.
public class DisallowStaticFieldsRule : BaseCustomRule
{
public DisallowStaticFieldsRule()
: base(typeof(DisallowStaticFieldsRule).Name)
{
}
public override ProblemCollection Check(Member member)
{
var field = member as Field;
if (field != null)
{
// Find all static data that isn't const or readonly
if (field.IsStatic && !field.IsInitOnly && !field.IsLiteral)
{
// field.FullName is an optional argument that will be
// used to format the Resolution string's {0} parameter.
var resolution = this.GetResolution(field.FullName);
var problem = new Problem(resolution,
field.SourceContext);
this.Problems.Add(problem);
}
}
return this.Problems;
}
}
The BaseCustomRule
class provides a number of virtual Check
method overrides with various types of arguments which you can override to provide your functionality. By default, these methods do nothing. IntelliSense is your friend while writing FxCop rules, and it reveals the following Check methods:
Check(ModuleNode moduleNode)
Check(Parameter parameter)
Check(Resource resource)
Check(TypeNode typeNode)
Check(string namespaceName, TypeNodeCollection types)
You can also examine individual lines of IL code from any method. Here is a rule that prohibits string case conversion.
public class DisallowStringCaseConversionRule : BaseCustomRule
{
public DisallowStringCaseConversionRule()
: base(typeof(DisallowStringCaseConversionRule).Name)
{ }
public override ProblemCollection Check(Member member)
{
var method = member as Method;
if (method != null)
{
foreach (var instruction in method.Instructions)
{
if (instruction.OpCode == OpCode.Call
|| instruction.OpCode == OpCode.Calli
|| instruction.OpCode == OpCode.Callvirt)
{
var targetMethod = instruction.Value as Method;
if (targetMethod != null
&& (targetMethod.FullName == "System.String.ToUpper"
|| targetMethod.FullName == "System.String.ToLower"))
{
var resolution = this.GetResolution(method.FullName);
var problem = new Problem(resolution,
method.SourceContext);
this.Problems.Add(problem);
}
}
}
}
return this.Problems;
}
}
For a final example, look at a different way to tell FxCop to traverse the code. In addition to the Check
methods described previously, you can override dozens of Visit*
methods. These are called in a recursive descent through every node in the program graph, starting at the node you pick. You override just the Visit
methods you need. Here is an example that uses this to add a rule against instantiating a Thread
object:
public class DisallowThreadCreationRule : BaseCustomRule
{
public DisallowThreadCreationRule()
: base(typeof(DisallowThreadCreationRule).Name) { }
public override ProblemCollection Check(Member member)
{
var method = member as Method;
if (method != null)
{
VisitStatements(method.Body.Statements);
}
return base.Check(member);
}
public override void VisitConstruct(Construct construct)
{
if (construct != null)
{
var binding = construct.Constructor as MemberBinding;
if (binding != null)
{
var instanceInitializer =
binding.BoundMember as InstanceInitializer;
if (instanceInitializer.DeclaringType.FullName
== "System.Threading.Thread")
{
var problem = new Problem(this.GetResolution(),
construct.SourceContext);
this.Problems.Add(problem);
}
}
}
base.VisitConstruct(construct);
}
}
To use these rules in Visual Studio, build the FxCopRules.dll and copy it to your FxCop installation’s Rules folder (mine is C:Program Files (x86)Microsoft Visual Studio 14.0Team ToolsStatic Analysis ToolsFxCopRules
). In Visual Studio, go to another project’s properties (you can test this on the FxCopViolator sample project) and view the Code Analysis tab. Under Rule Set, you can select a custom rule set, or create your own that includes whatever rules you want.
Now, when you build a project with the appropriate rules selected, you should see some build messages indicating violations of your custom rules. Just like any standard build or code analysis rule, you can double-click these and jump straight to the source.
The sample project also includes an example of how to run FxCop with custom rules from the command line:
>"C:Program Files (x86)Microsoft Fxcop 10.0FxCopCmd.exe"
/out:.FxCopOutput.xml /rule:FxCopRules.dll
/file:FxCopViolator.dll
Microsoft (R) FxCop Command-Line Tool, Version 14.0 (14.0.25420.1)
Copyright (C) Microsoft Corporation, All Rights Reserved.
Loaded fxcoprules.dll...
Loaded FxCopViolator.dll...
Initializing Introspection engine...
Analyzing...
Analysis Complete.
Writing 3 messages...
Writing report to .FxCopOutput.xml...
Done:00:00:00.8200342
It is pretty straightforward once you learn how it works. The biggest obstacle to creating your own rules is really the paucity of official documentation. To learn more about custom FxCop rules, read an excellent walkthrough by Jason Kresowaty at http://www.binarycoder.net/fxcop/.
In contrast to FxCop’s limited understanding of code, .NET Compiler Code Analyzers can not only analyze high-level language code instead of IL, they can do so from within the Visual Studio IDE and they can even suggest and perform code edits. These types of analyzers replace, and are far more powerful than, FxCop. This section will walk through creating a couple rules, one that requires static
fields to also be marked readonly
, and another one to warn against calling String.ToLower
and String.ToUpper
.
In Visual Studio 2015, you will need to install the “Visual Studio Extensibility Tools” component from the installer. In Visual Studio 2017, it has a slightly different name: “Visual Studio extension development.”
In both versions, you will need to install “.NET Compiler Platform SDK,” which you can do from Visual Studio directly by going to File | New Project | Visual C# | Extensibility and selecting the “Download the .NET Compiler Platform SDK” in the list of project types. Once the installation is completed, restart Visual Studio. Then, to create your own analyzer, you can choose “Analyzer with Code Fix (NuGet + VSIX)” in the New Project selection window.
The example produced here is available in the CodeAnalyzers sample solution in the accompanying source code. There are three projects:
To test out the analyzer, make sure the Vsix project is selected as default, and hit F5. This will start another instance of Visual Studio with the analyzer loaded. You can create a new project in here to test out the code analysis.
Our first code analyzer will detect any static
field and recommend it be marked readonly
. In addition, it will contain a fix provider to actually do this for you.
The contents of StaticFieldAnalyzer.cs show how this is done:
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace SampleCodeAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class StaticFieldAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "StaticFieldAnalyzer";
private static readonly LocalizableString Title =
new LocalizableResourceString(
nameof(Resources.AnalyzerTitle),
Resources.ResourceManager,
typeof(Resources));
private static readonly LocalizableString MessageFormat =
new LocalizableResourceString(
nameof(Resources.AnalyzerMessageFormat),
Resources.ResourceManager,
typeof(Resources));
private static readonly LocalizableString Description =
new LocalizableResourceString(
nameof(Resources.AnalyzerDescription),
Resources.ResourceManager,
typeof(Resources));
private const string Category = "Thread Safety";
private static DiagnosticDescriptor Rule =
new DiagnosticDescriptor(DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: Description);
public override ImmutableArray<DiagnosticDescriptor>
SupportedDiagnostics
{
get
{
return ImmutableArray.Create(Rule);
}
}
public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(AnalyzeFieldSymbol,
SymbolKind.Field);
}
private void AnalyzeFieldSymbol(
SymbolAnalysisContext context)
{
IFieldSymbol field = (IFieldSymbol)context.Symbol;
if (field.IsStatic && !field.IsReadOnly)
{
var diagnostic = Diagnostic.Create(
Rule,
field.Locations[0],
field.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
}
The fields at the top of the class are standard boilerplate metadata you should customize for each rule. The project template puts these strings into Resources.resx by default to make them easier to localize, but there is no requirement that you do so.
The Initialize
method tells Visual Studio what kinds of things you want to analyze. In this case, we are analyzing just fields, but we will see another option later. The AnalyzeFieldSymbol
method is where the action is. This method is called for every symbol of the type we asked for. The code checks to see if the field is static
, but not readonly
, and if so it reports a new diagnostic, which will be surfaced in the user interface via a green squiggly line underneath the problematic symbol. The squiggly line is green because we marked the rule as DiagnosticSeverity.Info
.
In some cases, you may be able to automatically fix the code according to your recommendation. Doing so is not required, but it is nice if you can do it. The StaticFieldFixer.cs file contains this code to implement our readonly
fix automatically.
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace SampleCodeAnalyzer
{
[ExportCodeFixProvider(LanguageNames.CSharp,
Name = nameof(StaticFieldFixer)),
Shared]
public class StaticFieldFixer : CodeFixProvider
{
private const string title = "Make readonly";
public sealed override ImmutableArray<string>
FixableDiagnosticIds
{
get
{
return ImmutableArray.Create(
StaticFieldAnalyzer.DiagnosticId);
}
}
public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(
CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(
context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Find the type declaration identified
// by the diagnostic.
var declaration = root.FindToken(diagnosticSpan.Start)
.Parent.AncestorsAndSelf()
.OfType<FieldDeclarationSyntax>()
.First();
// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument:
c => MakeReadOnlyAsync(context.Document,
declaration,
c),
equivalenceKey: title),
diagnostic);
}
private async Task<Document> MakeReadOnlyAsync(
Document document,
FieldDeclarationSyntax fieldDecl,
CancellationToken cancellationToken)
{
// Find the field and update its modifiers
var newFieldDecl = fieldDecl.AddModifiers(
SyntaxFactory.Token(
SyntaxKind.ReadOnlyKeyword));
var root = await document.GetSyntaxRootAsync();
// Replace the node with a new one
var newRoot = root.ReplaceNode(fieldDecl,
newFieldDecl);
var newDocument = document.WithSyntaxRoot(newRoot);
// Return the new solution with the
// now-uppercase type name.
return newDocument;
}
}
}
The FixableDiagnosticIds
property associates the analyzer with this fixer so Visual Studio knows what actions it can take. The RegisterCodeFixesAsync
method finds the diagnostic and registers a delegate to be called to fix the code. The MakeReadOnlyAsync
method is what does the real work. It returns a Document
object that represents the new code document, after the fixes it generates. In this case, it takes the field declaration and adds readonly
to the list of modifiers. The SyntaxFactory
class contains a wealth of options to create new pieces of code.
This code works by modifying the individual nodes of the document’s syntax tree. The syntax tree is immutable, so doing any modifications causes a new version of the object to be created and returned to you. The MakeReadOnlyAsync
method successively retrieves new versions of the field, the syntax node, and the document.
Look at another trivial example of an analyzer that recommends that you do not call the methods String.ToLower
and String.ToUpper
.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace SampleCodeAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class StringToUpperToLowerAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId =
"StringToUpperToLowerAnalyzer";
private static readonly LocalizableString Title =
new LocalizableResourceString(
nameof(Resources.ToUpperToLowerAnalyzerTitle),
Resources.ResourceManager,
typeof(Resources));
private static readonly LocalizableString MessageFormat =
new LocalizableResourceString(
nameof(Resources.ToUpperToLowerAnalyzerMessageFormat),
Resources.ResourceManager,
typeof(Resources));
private static readonly LocalizableString Description =
new LocalizableResourceString(
nameof(Resources.ToUpperToLowerAnalyzerDescription),
Resources.ResourceManager,
typeof(Resources));
private const string Category = "Performance";
private static DiagnosticDescriptor Rule =
new DiagnosticDescriptor(DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description);
public override ImmutableArray<DiagnosticDescriptor>
SupportedDiagnostics
{
get
{
return ImmutableArray.Create(Rule);
}
}
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(
AnalyzeNode,
SyntaxKind.InvocationExpression);
}
private void AnalyzeNode(
SyntaxNodeAnalysisContext context)
{
var invocationExpression =
(InvocationExpressionSyntax)context.Node;
var memberAccessExpression =
invocationExpression.Expression
as MemberAccessExpressionSyntax;
var memberName =
memberAccessExpression?.Name.ToString();
if (memberName == "ToUpper"
|| memberName == "ToLower")
{
var diagnostic = Diagnostic.Create(
Rule,
memberAccessExpression.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
}
One additional tool that can help as you develop analyzers is the Syntax Visualizer. You can install this in Visual Studio, via Tools | Extensions and Updates. Search for .NET Compiler Platform SDK, which includes the Syntax Visualizer. Once installed, open it from View | Other Windows | Syntax Visualizer. When it is open, you can click anywhere in a code file and see the syntax tree updated.
You can do almost anything in code analyzers. They are very flexible and allow you almost unlimited freedom to analyze your own code. Some examples:
As you gain experience and deeper understanding of both your own code and the way its syntax is represented by the compiler, you can iterate your analyzers to provide more complete and robust analysis.
Thankfully, you do not have to write everything yourself. There are a number of existing code analyzers out there for you to reuse, including:
These should all be easily searchable with your favorite search engine.
You should keep as much performance-sensitive code as possible in one place for easy maintenance, preferably behind APIs that the rest of your application uses. For example, if your application downloads files via HTTP, you could wrap this in an API that exposes only the parts of downloading that the rest of your program needs to know (e.g., the URL you are requesting and the downloaded content). The API manages the complexity of the HTTP call and your entire application goes through that API every time it needs to make an HTTP call. If you discover a performance problem with downloading, or need to enforce a download queue, or any other change, it is trivial to do behind the API. Remember that those APIs need to maintain the asynchronous nature of the operation.
For many reasons, you should move away from unmanaged code if at all possible. As discussed in the introduction, the benefits of unmanaged code are often exaggerated, but the danger of memory corruption is all too real.
That said, if you have to keep any unmanaged code around (say, to talk to a legacy system, and it is too expensive to move the entire interface to the managed world), then isolate it well. There are many ways to do the isolation, but you absolutely want to avoid having random bits of your system call into unmanaged code all over the place. This is a recipe for chaos.
Ideally, split the unmanaged code into its own process to provide strict OS-level isolation. If that is not possible and you need the unmanaged code to be loaded into the same process, try to keep it in as few DLLs as possible and have all calls to it go through a centralized API that can enforce standard safeguards.
Unmanaged code introduces significant risk into your process. Any bugs that corrupt memory on the unmanaged side of the process can corrupt anything in the process, including memory on the managed side. You lose the safety guarantees that the CLR provides.
Treat managed code that is marked unsafe exactly like unmanaged code and isolate it to as small a scope as you can. You will also need to enable unsafe code in the project settings.
Code readability and maintenance is more important than performance until proven otherwise. If you find you do need to make deep changes for performance reasons, do it in a way that is as transparent to the code above it as possible.
Once you do make the code worse to read in favor of performance, make sure you document in the code why you are doing it so that someone does not come by after you and “clean up” your elegant optimization by making it simpler.
To ensure your code is safe, you must understand the implementation details at all levels. Isolate your riskiest code, especially native or unsafe code, to specific modules to limit exposure. Ban problematic APIs and coding patterns and enforce reasonable code standards to encourage safe practices. Enforce these practices with code analyzers or other static analysis build tools. Do not sacrifice code clarity or maintainability for performance unless it is particularly justified.