Come velocizzare il ToString(), e non solo, degli enum

enum tostring

Introduzione

Il metodo ToString() di un enum è implementato in maniera molto discutibile: è lento e inoltre porta a delle inutili allocazioni di memoria che dovranno essere eliminati dal GC.

Andiamo a vedere sotto il cofano come funziona il metodo ToString(); dopo qualche metodo interno otteniamo:

private static string? GetEnumName(EnumInfo enumInfo, ulong ulValue)
{
    int index = Array.BinarySearch(enumInfo.Values, ulValue);
    if (index >= 0)
    {
        return enumInfo.Names[index];
    }

    return null; // return null so the caller knows to .ToString() the input
}

Come si nota vi è un binary search su tutti i valori dell’enum e inoltre un accesso tramite indice all’array contenente i nomi.

Gli algoritmi di ricerca binaria hanno performance di O(logn), quindi più sono i valori dell’enum maggiore sarà il tempo impiegato dall’algoritmo a trovare il valore corretto.

Questa complicazione può essere completamente evitata dato che il ToString() di un Enum è, di fatto, il suo nameof: il ToString() di Color.Aquamarine è esattamente nameof(Color.Aquamarine)

Questa chiamata è incredibilmente più veloce, non dipende dalla dimensione dell’enum e inoltre non alloca memoria.

Il problema è che, per ogni enum da velocizzare, servirebbe che ci sia un metodo con all’interno un gigantesco switch case che mappa ogni valore dell’enum al suo nameof.

Il primo approccio è fare tutto a mano ma, grazie ai Source Generators, è possibile automatizzare il lavoro.

Andrew Lock, nel suo blog .NET Escapades, ha creato un comodo pacchetto nuget per automatizzare la generazione di codice veloce per ogni enum che si vuole. Il progetto è open source e disponibile qui: EnumGenerators.

Una volta importato il pacchetto nuget è solo necessario aggiungere l’attributo [EnumExtensions] sopra l’enum da velocizzare e verranno generati degli extension methods automaticamente.

Test

Oltre a ToString() ho testato Enum.IsDefined e Enum.TryParse utilizzando BenchmarkDotNet confrontando le loro performance con i metodi classici.

[Benchmark]
public string EnumToString()
{
    return EnumColor.Aquamarine.ToString();
}

[Benchmark]
public string EnumToStringFast()
{
    return EnumColor.Aquamarine.ToStringFast();
}

[Benchmark]
public bool EnumIsDefined()
{
    return Enum.IsDefined(typeof(EnumColor), 48);
}

[Benchmark]
public bool EnumIsDefinedFast()
{
    return EnumColorExtensions.IsDefined((EnumColor)48);
}

[Benchmark]
public (bool, EnumColor) EnumTryParse()
{
    var couldParse = Enum.TryParse("Aquamarine", false, out EnumColor value);
    return (couldParse, value);
}

[Benchmark]
public (bool, EnumColor) EnumTryParseFast()
{
    var couldParse = EnumColorExtensions.TryParse("Aquamarine", false, out var value);
    return (couldParse, value);
}

Ecco i risultati:

|            Method |       Mean |         Error |     StdDev |  Gen 0 |  Gen 1 | Allocated |
|------------------ |-----------:|--------------:|-----------:|-------:|-------:|----------:|
|      EnumToString |  55.941 ns |    66.2153 ns |  3.6295 ns | 0.0057 | 0.0002 |      24 B |
|  EnumToStringFast |   1.512 ns |     1.8909 ns |  0.1036 ns |      - |      - |         - |
|     EnumIsDefined | 301.073 ns | 1,289.5731 ns | 70.6859 ns | 0.0057 |      - |      24 B |
| EnumIsDefinedFast |   1.813 ns |     0.3078 ns |  0.0169 ns |      - |      - |         - |
|      EnumTryParse | 190.173 ns |   414.8240 ns | 22.7379 ns |      - |      - |         - |
|  EnumTryParseFast |  20.076 ns |    21.5005 ns |  1.1785 ns |      - |      - |         - |

Conclusione

Come si nota abbiamo circa due ordini di grandezza di velocità e inoltre non abbiamo alcuna allocazione di memoria.

Analizzando l’extension ToStringFast() notiamo che internamente ha lo swtich case di cui parlavamo in precedenza che mappa ogni valore dell’enum nel suo nameof.

Dei limiti degli enum e di questo pacchetto ne ha parlato anche Nick Chapsas qui e qui.

NetEscapades.EnumGenerators è ancora in beta ed è richiede almeno .NET 6 SDK.

Indice

Share
Ultimi articoli
Join

Newsletter

Nessuno spam, solo articoli interessanti ;)

Focus

Post correlati

semaphoreslim

SmaphoreSlim 101

SemaphoreSlim è una classe che permette la sincronizzazione di n thread che hanno una risorsa (scarsa) condivisa limitandone l’uso ad un numero massimo.

interlocked

Interlocked 101

La sincronizzazione dei thread è un elemento fondamentale nella programmazione asincrona, ne ho infatti parlato in vari post. La soluzione più versatile è sicuramente utilizzare

event

Come testare gli eventi

Testare che degli eventi siano stato effettivamente lanciati in C# non è immediato. Tipicamente è possibile testare che un evento venga lanciato aspettando un ManualResetEvent

md5

Perché non dovresti usare MD5

Introduzione Le funzioni di hash permettono di ottenere una sequenza di bit tramite l’utilizzo di funzioni matematiche in base ai dati in ingresso; la loro

Codice Pragmatico

Contatti

Per informazioni, dubbi o consulenze non esitate a contattarmi.

Lascia un messaggio

Ricevi le ultime news

Iscrivi alla newsletter

Solo articoli interessanti, promesso ;)