Code Safety and Analysis

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.

Understanding the OS, APIs, and Hardware

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.

Restrict API Usage in Certain Areas of Your Code

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.

Custom FxCop Rules

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:

  • Prohibiting use of “dangerous” namespaces
  • Banning types or APIs that typically cause LOH allocations
  • Banning APIs that have better alternatives such as TryParse in lieu of Parse
  • Finding instances of double-casting
  • Finding instances of boxing
  • Enforcing specific usage patterns for types (e.g., all 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.

You can customize the rules you want to enforce in Visual Studio via the Code Analysis tab in a project’s properties.
You can customize the rules you want to enforce in Visual Studio via the Code Analysis tab in a project’s properties.

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.

Custom FxCop warnings will show up in the Error List pane, just like other build and code analysis issues.
Custom FxCop warnings will show up in the Error List pane, just like other build and code analysis issues.

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/.

.NET Compiler Code Analyzers

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:

  • SampleCodeAnalyzer.csproj: Contains the actual analysis and fixer code.
  • SampleCodeAnalyzer.Test.csproj: Unit tests to exercise your code without needing to debug in Visual Studio.
  • SampleCodeAnalyzer.Vsix: The Visual Studio add-on project that enables your analyzer to be hosted in Visual Studio.

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 Resources.resx file contains the localizable strings for your code analyzer.
The Resources.resx file contains the localizable strings for your code analyzer.

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.

The code analysis rule causes a squiggly line to appear underneath the syntax in question.
The code analysis rule causes a “squiggly” line to appear underneath the syntax in question.

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.

Clicking on the Code Tips icon brings up an option to automatically make the fix, with a preview of what will be changed.
Clicking on the Code Tips icon brings up an option to automatically make the fix, with a preview of what will be changed.

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);
            }
        }
    }
}
With code analyzers, you can encourage good programming practices with direct feedback in Visual Studio.
With code analyzers, you can encourage good programming practices with direct feedback in Visual Studio.

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.

The Syntax Visualizer tool can help you understand code’s structure as you develop analysis rules.
The Syntax Visualizer tool can help you understand code’s structure as you develop analysis rules.

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:

  • Parse, compile, and pre-execute the code, as you type it, analyzing the results according to whatever rules you want.
  • Analyze string literals for semantic correctness (such as the illegal presence of credentials, or regular expression validity).
  • Enforce coding guidelines particular to your team or company.
  • Enforce performance standards specific to your product.

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:

  • Roslyn Analyzers: These ship with the compiler. Browsing the source code will teach you a lot about writing analyzers.
  • Clr Heap Allocation Analyzer: Highlights every source of heap allocation, including implicit allocations.
  • StyleCopAnalyzers: Coding style recommendations.
  • and many more…

These should all be easily searchable with your favorite search engine.

Centralize and Abstract Performance-Sensitive and Difficult Code

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.

Isolate Unmanaged and Unsafe Code

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.

Isolate unsafe code into known regions in your application. Subject them to extra scrutiny.
Isolate unsafe code into known regions in your application. Subject them to extra scrutiny.

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.

Prefer Code Clarity to Performance Until Proven Otherwise

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset