Best practices for improving performance in C #

Hello everyone. We have prepared a translation of another useful material on the eve of the start of the course "C # Developer" . Enjoy reading.




Since I recently made a list of best practices in C # for Criteo, I thought it would be nice to share it publicly. The purpose of this article is to provide an incomplete list of code templates that should be avoided, either because they are questionable, or because they just work poorly. The list may seem a bit random because it is slightly taken out of context, but all of its elements were at some point found in our code and caused problems in production. I hope this will serve as a good prevention and prevent your mistakes in the future.

Also note that Criteo web services rely on high-performance code, hence the need to avoid inefficient code. In most applications, there will be no noticeable tangible difference from replacing some of these templates.

And last, but not least, some points (for example, ConfigureAwait) have already been discussed in many articles, so I will not dwell on them in detail. The goal is to form a compact list of points to which you need to pay attention, and not give a detailed technical description of each of them.

Synchronously waiting for asynchronous code


Never expect synchronously unfinished tasks. This applies to, but is not limited to: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll.

As a generalization: any synchronous relationship between two pool threads can cause pool depletion. The causes of this phenomenon are described in this article .

ConfigureAwait


If your code can be called from a synchronization context, use ConfigureAwait(false)for each of your await calls.

Please note that this is ConfigureAwait only useful when using a keyword await.

For example, the following code is meaningless:

//  ConfigureAwait        
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();

async void


Never useasync void . The exception thrown in the async voidmethod propagates to the synchronization context and usually causes the entire application to crash.

If you cannot return the task in your method (for example, because you are implementing the interface), move the asynchronous code to another method and call it:

interface IInterface
{
    void DoSomething();
}

class Implementation : IInterface
{
    public void DoSomething()
    {
        //      ,
        //      
        _ = DoSomethingAsync();
    }

    private async Task DoSomethingAsync()
    {
        await Task.Delay(100);
    }
}

Avoid async whenever possible


Out of habit or because of muscle memory, you can write something like:

public async Task CallAsync()
{
    var client = new Client();
    return await client.GetAsync();
}

Although the code is semantically correct, using a keyword asyncis not required here and can result in significant overhead in a highly loaded environment. Try to avoid it whenever possible:

public Task CallAsync()
{
    var client = new Client();
    return _client.GetAsync();
}

However, keep in mind that you cannot resort to this optimization when your code is wrapped in blocks (for example, try/catchor using):

public async Task Correct()
{
    using (var client = new Client())
    {
        return await client.GetAsync();
    }
}

public Task Incorrect()
{
    using (var client = new Client())
    {
        return client.GetAsync();
    }
}

In the wrong version ( Incorrect()), the client may be deleted before the GetAsynccall is completed , since the task inside the using block is not expected by await.

Regional comparisons


If you have no reason to use regional comparisons, always use ordinal comparisons . Although, due to internal optimizations, this does not really matter for the en-US data presentation forms, the comparison is an order of magnitude slower for the presentation forms of other regions (and up to two orders of magnitude on Linux!). Since string comparison is a frequent operation in most applications, the overhead increases significantly.

ConcurrentBag <T>


Never use ConcurrentBag<T>without benchmarking . This collection was designed for very specific use cases (when most of the time the item is excluded from the queue by the thread that queued it) and suffers from serious performance problems if it is used for other purposes. If you need a thread-safe collection, prefer ConcurrentQueue <T> .

ReaderWriterLock / ReaderWriterLockSlim <T >
Never use without benchmarking. ReaderWriterLock<T>/ReaderWriterLockSlim<T>Although using this kind of specialized synchronization primitive when working with readers and writers can be tempting, its cost is much higher than simple Monitor(used with a keyword lock). If the number of readers performing the critical section at the same time is not very large, concurrency will not be enough to absorb the increased overhead, and the code will work worse.

Prefer lambda functions instead of method groups


Consider the following code:

public IEnumerable<int> GetItems()
{
    return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Resharper suggests rewriting code without a lambda function, which might look a little cleaner:

public IEnumerable<int> GetItems()
{
    return _list.Where(Filter);
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Unfortunately, this leads to the allocation of dynamic memory for each call. In fact, the call compiles as:

public IEnumerable<int> GetItems()
{
    return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

This can have a significant impact on performance if the code is called in a heavily loaded section.

Using lambda functions starts compiler optimization, which caches the delegate in a static field, avoiding allocation. This only works if Filterstatic. If not, you can cache the delegate yourself:

private Predicate<int> _filter;

public Constructor()
{
    _filter = new Predicate<int>(Filter);
}

public IEnumerable<int> GetItems()
{
    return _list.Where(_filter);
}

private bool Filter(int element)
{
    return i % 2 == 0;
}

Convert Enumerations to Strings


Calling Enum.ToStringin .netis quite expensive, because reflection is used to convert inside, and calling the virtual method on the structure provokes packaging. This should be avoided as much as possible.

Enumerations can often be replaced with constant strings:

//       Numbers.One, Numbers.Two, ...
public enum Numbers
{
    One,
    Two,
    Three
}

public static class Numbers
{
    public const string One = "One";
    public const string Two = "Two";
    public const string Three = "Three";
}

If you really need to use an enumeration, consider caching the converted value in a dictionary to amortize overhead.

Enumeration Comparison


Note: this is no longer relevant in .net core, since version 2.1, optimization is performed by JIT automatically.

When using enumerations as flags, it may be tempting to use the method Enum.HasFlag:

[Flags]
public enum Options
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4
}

private Options _option;

public bool IsOption2Enabled()
{
    return _option.HasFlag(Options.Option2);
}

This code provokes two packages with allocation: one for conversion Options.Option2to Enum, and the other for a virtual call HasFlagfor the structure. This makes this code disproportionately expensive. Instead, you should sacrifice readability and use binary operators:

public bool IsOption2Enabled()
{
    return (_option & Options.Option2) == Options.Option2;
}

Implementation of comparison methods for structures


When using a structure in comparisons (for example, when used as a key for a dictionary), you need to override the methods Equals/GetHashCode. The default implementation uses reflection and is very slow. The implementation generated by Resharper is usually pretty good.

You can learn more about this here: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-c

Avoid inappropriate packaging when using structures with interfaces


Consider the following code:

public class IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue(IValue value)
{
    // ...
}

public void LogValue(IValue value)
{
    // ...
}

Make IntValuestructured can be tempting to avoid allocating dynamic memory. But since AddValuethey SendValueexpect an interface, and the interfaces have reference semantics, the value will be packaged with each call, negating the advantages of this "optimization". In fact, there will be even more memory allocations than if it IntValuewere a class, since the value will be packaged independently for each call.

If you are writing an API and expect some values ​​to be structures, try using generic methods:

public struct IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue<T>(T value) where T : IValue
{
    // ...
}

public void LogValue<T>(T value) where T : IValue
{
    // ...
}

Although the conversion of these methods to universal seems useless at first glance, it actually allows you to avoid packaging with allocation in the case when it IntValueis a structure.

CancellationToken Subscriptions Always Inline


When you cancel CancellationTokenSource, all subscriptions will be executed inside the current thread. This can lead to unplanned pauses or even implicit deadlocks.

var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel(); //     5 

You cannot escape this behavior. Therefore, when canceling CancellationTokenSource, ask yourself if you can safely allow your current thread to be captured. If the answer is no, wrap the call Cancelinside Task.Runto execute it in the thread pool.

TaskCompletionSource continuations often inline


Like subscriptions CancellationToken, continuations TaskCompletionSourceare often inline. This is a good optimization, but it can cause implicit errors. For example, consider the following program:

class Program
{
    private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
    
    public static async Task Deadlock()
    {
        await ProcessAsync();
        _mutex.Wait();
    }
    
    private static Task ProcessAsync()
    {
        var tcs = new TaskCompletionSource<bool>();
        
        Task.Run(() =>
        {
            Thread.Sleep(2000); //  - 
            tcs.SetResult(true);
            _mutex.Set();
        });
        
        return tcs.Task;
    }
    
    static void Main(string[] args)
    {
        Deadlock().Wait();
        Console.WriteLine("Will never get there");
    }
}

The call tcs.SetResultcauses the continuation await ProcessAsync()to execute in the current thread. Therefore, the statement _mutex.Wait()is executed by the same thread that it should call _mutex.Set(), which leads to a deadlock. This can be avoided by passing parameter TaskCreationsOptions.RunContinuationsAsynchronouslyc TaskCompletionSource.

If you have no good reason to neglect it, always use the option TaskCreationsOptions.RunContinuationsAsynchronouslywhen creating TaskCompletionSource.

Be careful: the code will also compile if you use TaskContinuationOptions.RunContinuationsAsynchronouslyinstead TaskCreationOptions.RunContinuationsAsynchronously, but the parameters will be ignored, and the continuations will still be inline. This is a surprisingly common mistake because it TaskContinuationOptionsprecedes TaskCreationOptionsautocomplete.

Task.Run / Task.Factory.StartNew


If you have no reason to use Task.Factory.StartNew, always choose Task.Runto run a background task. Task.Runuses safer default values, and more importantly, it automatically unpacks the returned task, which can prevent unobvious errors with asynchronous methods. Consider the following program:

class Program
{
    public static async Task ProcessAsync()
    {
        await Task.Delay(2000);
        Console.WriteLine("Processing done");
    }
    
    static async Task Main(string[] args)
    {
        await Task.Factory.StartNew(ProcessAsync);
        Console.WriteLine("End of program");
        Console.ReadLine();
    }
}

Despite its appearance, the End of program will be displayed earlier than Processing done. This is because it Task.Factory.StartNewwill return Task<Task>, and the code expects only an external task. The correct code could be either await Task.Factory.StartNew(ProcessAsync).Unwrap(), or await Task.Run(ProcessAsync).

There are only three valid use cases Task.Factory.StartNew:

  • Running a task in another scheduler.
  • Performing a task in a dedicated thread (using TaskCreationOptions.LongRunning).
  • ( TaskCreationOptions.PreferFairness).



.



All Articles