When handling the values of an enum on a case by case basis, is it better to use a switch statement or a dictionary?
I would think that the dictionary would be faster.
In fact, a dictionary is slower. Really. Just write simple benchmark (I've added example with converting dictionary to array):
void Main()
{
for (int itFac = 0; itFac < 7; itFac++ ) {
var iterations = 100;
iterations *= (int)Math.Pow(10, itFac);
Console.WriteLine("Iterations: {0}", iterations);
{
Random r = new Random();
int maxFruits = 5;
var timer = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++) {
var res = Fruits.GetSpanishEquivalentWithArray((Fruits.FruitType)r.Next(maxFruits));
}
Console.WriteLine("Array time: {0}", timer.Elapsed);
}
{
Random r = new Random();
int maxFruits = 5;
var timer = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++) {
var res = Fruits.GetSpanishEquivalent((Fruits.FruitType)r.Next(maxFruits));
}
Console.WriteLine("Switch time : {0}", timer.Elapsed);
}
{
Random r = new Random();
int maxFruits = 5;
var timer = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++) {
var res = Fruits.GetSpanishEquivalentWithDictionary((Fruits.FruitType)r.Next(maxFruits));
}
Console.WriteLine("Dictionary time: {0}", timer.Elapsed);
}
Console.WriteLine();
}
}
class Fruits {
public enum FruitType
{
Other,
Apple,
Banana,
Mango,
Orange
}
public enum SpanishFruitType
{
Otra,
Manzana, // Apple
Naranja, // Orange
Platano, // Banana
// let's say they don't have mangos, because I don't remember the word for it.
}
public static SpanishFruitType GetSpanishEquivalent(FruitType typeOfFruit)
{
switch(typeOfFruit)
{
case FruitType.Apple:
return SpanishFruitType.Manzana;
case FruitType.Banana:
return SpanishFruitType.Platano;
case FruitType.Orange:
return SpanishFruitType.Naranja;
case FruitType.Mango:
case FruitType.Other:
return SpanishFruitType.Otra;
default:
throw new Exception("what kind of fruit is " + typeOfFruit + "?!");
}
}
public static SpanishFruitType GetSpanishEquivalent(string typeOfFruit)
{
switch(typeOfFruit)
{
case "apple":
return SpanishFruitType.Manzana;
case "banana":
return SpanishFruitType.Platano;
case "orange":
return SpanishFruitType.Naranja;
case "mango":
case "other":
return SpanishFruitType.Otra;
default:
throw new Exception("what kind of fruit is " + typeOfFruit + "?!");
}
}
public static Dictionary<FruitType, SpanishFruitType> EnglishToSpanishFruit = new Dictionary<FruitType, SpanishFruitType>()
{
{FruitType.Apple, SpanishFruitType.Manzana}
,{FruitType.Banana, SpanishFruitType.Platano}
,{FruitType.Mango, SpanishFruitType.Otra}
,{FruitType.Orange, SpanishFruitType.Naranja}
,{FruitType.Other, SpanishFruitType.Otra}
};
public static SpanishFruitType GetSpanishEquivalentWithDictionary(FruitType typeOfFruit)
{
return EnglishToSpanishFruit[typeOfFruit]; // throws exception if it's not in the dictionary, which is fine.
}
public static SpanishFruitType[] EnglishToSpanishFruitArray;
static Fruits() {
EnglishToSpanishFruitArray = new SpanishFruitType[EnglishToSpanishFruit.Select(p => (int)p.Key).Max() + 1];
foreach (var pair in EnglishToSpanishFruit)
EnglishToSpanishFruitArray[(int)pair.Key] = pair.Value;
}
public static SpanishFruitType GetSpanishEquivalentWithArray(FruitType typeOfFruit)
{
return EnglishToSpanishFruitArray[(int)typeOfFruit]; // throws exception if it's not in the dictionary, which is fine.
}
}
Results:
Iterations: 100
Array time : 00:00:00.0108628
Switch time : 00:00:00.0002204
Dictionary time: 00:00:00.0008475
Iterations: 1000
Array time : 00:00:00.0000410
Switch time : 00:00:00.0000472
Dictionary time: 00:00:00.0004556
Iterations: 10000
Array time : 00:00:00.0006095
Switch time : 00:00:00.0011230
Dictionary time: 00:00:00.0074769
Iterations: 100000
Array time : 00:00:00.0043019
Switch time : 00:00:00.0047117
Dictionary time: 00:00:00.0611122
Iterations: 1000000
Array time : 00:00:00.0468998
Switch time : 00:00:00.0520848
Dictionary time: 00:00:00.5861588
Iterations: 10000000
Array time : 00:00:00.4268453
Switch time : 00:00:00.5002004
Dictionary time: 00:00:07.5352484
Iterations: 100000000
Array time : 00:00:04.1720282
Switch time : 00:00:04.9347176
Dictionary time: 00:00:56.0107932
What happens. Let's look on the generated IL code:
Fruits.GetSpanishEquivalent:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: stloc.1
IL_0003: ldloc.1
IL_0004: switch (IL_002B, IL_001F, IL_0023, IL_002B, IL_0027)
IL_001D: br.s IL_002F
IL_001F: ldc.i4.1
IL_0020: stloc.0
IL_0021: br.s IL_004A
IL_0023: ldc.i4.3
IL_0024: stloc.0
IL_0025: br.s IL_004A
IL_0027: ldc.i4.2
IL_0028: stloc.0
IL_0029: br.s IL_004A
IL_002B: ldc.i4.0
IL_002C: stloc.0
IL_002D: br.s IL_004A
IL_002F: ldstr "what kind of fruit is "
IL_0034: ldarg.0
IL_0035: box UserQuery+Fruits.FruitType
IL_003A: ldstr "?!"
IL_003F: call System.String.Concat
IL_0044: newobj System.Exception..ctor
IL_0049: throw
IL_004A: ldloc.0
IL_004B: ret
What happens? Switch happens. For sequenced number of values switch can be optimized and replaced by jump to pointer from array. Why real array works faster than switch - dunno, it just works faster.
Well, if you do not work with enums, but with strings there is no real difference between switch and dictionary on small number of variants. With more and more variants dictionary becomes faster.
What to choose? Choose what is easier to read for you and your team. When you see that your solution creates performance issues you should replace Dictionary (if you use it) to switch or array like me. When your function of translation is rarely called there is no need to optimize it.
Talking about your case - to get translation, all solutions are bad. Translations must be stored in resources. There must be only one FruitType, no other enums.
Since the translations are one-to-one or one-to-none, why not assign IDs to each word. Then cast from one enum to another
So define your enums as
enum FruitType
{
Other = 0,
Apple = 1,
Banana = 2,
Mango = 3,
Orange = 4
}
enum SpanishFruitType
{
Otra = 0,
Manzana = 1, // Apple
Platano = 2, // Banana
Naranja = 4, // Orange
}
Then define your conversion method as
private static SpanishFruitType GetSpanishEquivalent(FruitType typeOfFruit)
{
//'translate' with the word's ID.
//If there is no translation, the enum would be undefined
SpanishFruitType translation = (SpanishFruitType)(int)typeOfFruit;
//Check if the translation is defined
if (Enum.IsDefined(typeof(SpanishFruitType), translation))
{
return translation;
}
else
{
return SpanishFruitType.Otra;
}
}
This question seems to be looking for the fastest method of retrieving an item which is mapped to an Enum
constant.
The underlying type for almost all Enum
types, which are not bit fields (i.e. not declared as [Flags]
), is a 32-bit signed integer. There are sound performance reasons for this. The only real reason to use something different is if you absolutely must minimise memory usage. Bit fields are a different matter but we're not concerned with them here.
In this typical scenario, an array map is ideal (and usually faster than a switch
). Here's some generic code which is concise and optimised for retrieval. Unfortunately, due to the limitations of .NET generic constraints, a few hacks are required (such as having to pass a casting delegate into the instance constructor).
using System;
using System.Runtime.CompilerServices;
namespace DEMO
{
public sealed class EnumMapper<TKey, TValue> where TKey : struct, IConvertible
{
private struct FlaggedValue<T>
{
public bool flag;
public T value;
}
private static readonly int size;
private readonly Func<TKey, int> func;
private FlaggedValue<TValue>[] flaggedValues;
public TValue this[TKey key]
{
get
{
int index = this.func.Invoke(key);
FlaggedValue<TValue> flaggedValue = this.flaggedValues[index];
if (flaggedValue.flag == false)
{
EnumMapper<TKey, TValue>.ThrowNoMappingException(); // Don't want the exception code in the method. Make this callsite as small as possible to promote JIT inlining and squeeze out every last bit of performance.
}
return flaggedValue.value;
}
}
static EnumMapper()
{
Type keyType = typeof(TKey);
if (keyType.IsEnum == false)
{
throw new Exception("The key type [" + keyType.AssemblyQualifiedName + "] is not an enumeration.");
}
Type underlyingType = Enum.GetUnderlyingType(keyType);
if (underlyingType != typeof(int))
{
throw new Exception("The key type's underlying type [" + underlyingType.AssemblyQualifiedName + "] is not a 32-bit signed integer.");
}
var values = (int[])Enum.GetValues(keyType);
int maxValue = 0;
foreach (int value in values)
{
if (value < 0)
{
throw new Exception("The key type has a constant with a negative value.");
}
if (value > maxValue)
{
maxValue = value;
}
}
EnumMapper<TKey, TValue>.size = maxValue + 1;
}
public EnumMapper(Func<TKey, int> func)
{
if (func == null)
{
throw new ArgumentNullException("func",
"The func cannot be a null reference.");
}
this.func = func;
this.flaggedValues = new FlaggedValue<TValue>[EnumMapper<TKey, TValue>.size];
}
public static EnumMapper<TKey, TValue> Construct(Func<TKey, int> func)
{
return new EnumMapper<TKey, TValue>(func);
}
public EnumMapper<TKey, TValue> Map(TKey key,
TValue value)
{
int index = this.func.Invoke(key);
FlaggedValue<TValue> flaggedValue;
flaggedValue.flag = true;
flaggedValue.value = value;
this.flaggedValues[index] = flaggedValue;
return this;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowNoMappingException()
{
throw new Exception("No mapping exists corresponding to the key.");
}
}
}
You can then simply initialise the mappings using a nice fluent interface:
var mapper = EnumMapper<EnumType, ValueType>.Construct((x) => (int)x)
.Map(EnumType.Constant1, value1)
.Map(EnumType.Constant2, value2)
.Map(EnumType.Constant3, value3)
.Map(EnumType.Constant4, value4)
.Map(EnumType.Constant5, value5);
And easily retrieve the mapped value:
ValueType value = mapper[EnumType.Constant3];
The x86 assembly (generated using the Visual Studio 2013 compiler) for the retrieval method is minimal:
000007FE8E9909B0 push rsi
000007FE8E9909B1 sub rsp,20h
000007FE8E9909B5 mov rsi,rcx
000007FE8E9909B8 mov rax,qword ptr [rsi+8]
000007FE8E9909BC mov rcx,qword ptr [rax+8]
000007FE8E9909C0 call qword ptr [rax+18h] // The casting delegate's callsite is optimised to just two instructions
000007FE8E9909C3 mov rdx,qword ptr [rsi+10h]
000007FE8E9909C7 mov ecx,dword ptr [rdx+8]
000007FE8E9909CA cmp eax,ecx
000007FE8E9909CC jae 000007FE8E9909ED
000007FE8E9909CE movsxd rax,eax
000007FE8E9909D1 lea rax,[rdx+rax*8+10h]
000007FE8E9909D6 movzx edx,byte ptr [rax]
000007FE8E9909D9 mov esi,dword ptr [rax+4]
000007FE8E9909DC test dl,dl
000007FE8E9909DE jne 000007FE8E9909E5
000007FE8E9909E0 call 000007FE8E9901B8
000007FE8E9909E5 mov eax,esi
000007FE8E9909E7 add rsp,20h
000007FE8E9909EB pop rsi
000007FE8E9909EC ret
000007FE8E9909ED call 000007FEEE411A08
000007FE8E9909F2 int 3
It really depends on your scenario but, as an alternative, you could just have an attribute containing the translated text.
public enum FruitType
{
[Description("Otra")]
Other,
[Description("Manzana")]
Apple,
[Description("Platano")]
Banana,
Mango,
[Description("Naranja")]
Orange
}
Then you can have a method to read the description
public static string GetTranslation(FruitType fruit)
{
var mi = typeof(FruitType).GetMember(fruit.ToString());
var attr = mi[0].GetCustomAttributes(typeof(DescriptionAttribute),false);
if (attr.Count() > 0)
return ((DescriptionAttribute)attr[0]).Description;
else
return fruit.ToString(); //if no description added, return the original fruit
}
So you can then call it like this
string translated = GetTranslation(FruitType.Apple);
Since it uses reflection, this is likely to be the least efficient but might be easier to maintain depending on your situation and, as Chris mentioned in the comments, may not have any noticeable impact depending on how often it's called. You can swap Description
for a custom attribute of course. Just another option for you to consider :)