Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated |
---|---|---|---|---|---|---|
GetUserRoleIfElse | 10.62 ns | 0.129 ns | 0.114 ns | 1.00 | - | - |
GetUserRoleProposedDictionary | 58.64 ns | 0.974 ns | 0.911 ns | 5.52 | 0.0215 | 216 B |
GetUserRoleStaticDictionary | 12.10 ns | 0.070 ns | 0.058 ns | 1.14 | - | - |
GetUserRoleEnumGetName | 41.87 ns | 0.755 ns | 0.706 ns | 3.94 | 0.0042 | 42 B |
GetUserRoleEnumToString | 26.46 ns | 0.453 ns | 0.402 ns | 2.49 | 0.0024 | 24 B |
GetUserRoleEnumSwitchStatement | 14.72 ns | 0.278 ns | 0.260 ns | 1.39 | - | - |
GetUserRoleEnumSwitchExpression | 14.90 ns | 0.190 ns | 0.178 ns | 1.40 | - | - |
GetUserRoleDictionary | 12.25 ns | 0.220 ns | 0.195 ns | 1.15 | - | - |
GetUserRoleSWitchExpression | 13.85 ns | 0.107 ns | 0.089 ns | 1.30 | - | - |
GetUserRoleSWitchStatement | 13.63 ns | 0.109 ns | 0.097 ns | 1.28 | - | - |
-
-
Save RobThree/5b84502f89a00dcbcddcabceec52c12b to your computer and use it in GitHub Desktop.
using BenchmarkDotNet.Attributes; | |
using BenchmarkDotNet.Running; | |
namespace IshaFatima; | |
internal class Program | |
{ | |
private static void Main(string[] args) | |
{ | |
var summary = BenchmarkRunner.Run<RoleBenchmark>(); | |
} | |
} | |
[MemoryDiagnoser] | |
public class RoleBenchmark | |
{ | |
private static readonly Dictionary<int, string> _roles = new() | |
{ | |
{1, "Admin"}, | |
{2, "Editor"}, | |
{3, "Viewer"} | |
}; | |
private static readonly Dictionary<UserRole, string> _roleenums = new() | |
{ | |
{UserRole.Admin, "Admin"}, | |
{UserRole.Editor, "Editor"}, | |
{UserRole.Viewer, "Viewer"} | |
}; | |
public enum UserRole | |
{ | |
Admin = 1, | |
Editor = 2, | |
Viewer = 3 | |
} | |
// For the enum-based benchmarks we would have to case our random value to the enum. To make everything fair we | |
// will generate a random value and for both the enum and int-based benchmarks we will a lookup in the arrays | |
// below to get the value to use in the benchmark so that we are comparing apples to apples and not skewing | |
// the results with casting for the enum-based benchmarks. | |
private static readonly int[] _role_ids = [1, 2, 3, 4]; // 1,2,3 are valid role ids, 4 should return "Unknown" | |
private static readonly UserRole[] _role_enums = [UserRole.Admin, UserRole.Editor, UserRole.Viewer, (UserRole)4]; | |
// This is the baseline benchmark that we will compare all other benchmarks to. This is the "wrong" way to do it | |
// according to Isha Fatima's LinkedIn post. | |
[Benchmark(Baseline = true)] | |
public void GetUserRoleIfElseBenchmark() | |
=> GetUserRoleIfElse(_role_ids[Random.Shared.Next(4)]); | |
public static string GetUserRoleIfElse(int roleId) | |
{ | |
if (roleId == 1) return "Admin"; | |
else if (roleId == 2) return "Editor"; | |
else if (roleId == 3) return "Viewer"; | |
return "Unknown"; | |
} | |
// According to Isha Fatima's LinkedIn post this is the "right" way to do it. | |
[Benchmark] | |
public void GetUserRoleProposedDictionaryBenchmark() | |
=> GetUserRoleProposedDictionary(_role_ids[Random.Shared.Next(4)]); | |
public static string GetUserRoleProposedDictionary(int roleId) | |
{ | |
var roles = new Dictionary<int, string> | |
{ | |
{1, "Admin"}, | |
{2, "Editor"}, | |
{3, "Viewer"} | |
}; | |
return roles.TryGetValue(roleId, out var role) ? role : "Unknown"; | |
} | |
// This is the same as the above benchmark but using a static dictionary instead of creating a new one each time. | |
// It could be argued this is what auther _actually_ meant in the post (but that's not what was written). | |
[Benchmark] | |
public void GetUserRoleStaticDictionaryBenchmark() | |
=> GetUserRoleStaticDictionary(_role_ids[Random.Shared.Next(4)]); | |
public static string GetUserRoleStaticDictionary(int roleId) | |
=> _roles.TryGetValue(roleId, out var role) ? role : "Unknown"; | |
// This variation gets the name of the enum value using Enum.GetName. | |
[Benchmark] | |
public void GetUserRoleEnumGetNameBenchmark() | |
=> GetUserRoleEnumGetName(_role_ids[Random.Shared.Next(4)]); | |
public static string GetUserRoleEnumGetName(int roleId) | |
=> Enum.IsDefined(typeof(UserRole), roleId) ? Enum.GetName(typeof(UserRole), roleId)! : "Unknown"; | |
// This variation gets the name of the enum value using Enum.ToString(). | |
[Benchmark] | |
public void GetUserRoleEnumToStringBenchmark() | |
=> GetUserRoleEnumToString(_role_enums[Random.Shared.Next(4)]); | |
public static string GetUserRoleEnumToString(UserRole role) | |
=> role.ToString(); | |
// This variation uses a switch statement with the enum. | |
[Benchmark] | |
public void GetUserRoleEnumSwitchStatementBenchmark() | |
=> GetUserRoleEnumSwitchStatement(_role_enums[Random.Shared.Next(4)]); | |
public static string GetUserRoleEnumSwitchStatement(UserRole role) { | |
switch (role) | |
{ | |
case UserRole.Admin: return "Admin"; | |
case UserRole.Editor: return "Editor"; | |
case UserRole.Viewer: return "Viewer"; | |
default: return "Unknown"; | |
}; | |
} | |
// This variation uses a switch expression with the enum. | |
[Benchmark] | |
public void GetUserRoleEnumSwitchExpressionBenchmark() | |
=> GetUserRoleEnumSwitchExpression(_role_enums[Random.Shared.Next(4)]); | |
public static string GetUserRoleEnumSwitchExpression(UserRole role) | |
=> role switch | |
{ | |
UserRole.Admin => "Admin", | |
UserRole.Editor => "Editor", | |
UserRole.Viewer => "Viewer", | |
_ => "Unknown" | |
}; | |
// This variation uses a dictionary with the enum. | |
[Benchmark] | |
public void GetUserRoleDictionaryBenchmark() | |
=> GetUserRoleDictionary(_role_enums[Random.Shared.Next(4)]); | |
public static string GetUserRoleDictionary(UserRole role) | |
=> _roleenums.TryGetValue(role, out var result) ? result : "Unknown"; | |
// This variation uses a switch expression with an int. | |
[Benchmark] | |
public void GetUserRoleSWitchExpressionBenchmark() | |
=> GetUserRoleSWitchExpression(_role_ids[Random.Shared.Next(4)]); | |
public static string GetUserRoleSWitchExpression(int roleId) | |
=> roleId switch | |
{ | |
1 => "Admin", | |
2 => "Editor", | |
3 => "Viewer", | |
_ => "Unknown" | |
}; | |
// This variation uses a switch statement with an int. | |
[Benchmark] | |
public void GetUserRoleSWitchStatementBenchmark() | |
=> GetUserRoleSWitchStatement(_role_ids[Random.Shared.Next(4)]); | |
public static string GetUserRoleSWitchStatement(int roleId) | |
{ | |
switch (roleId) | |
{ | |
case 1: return "Admin"; | |
case 2: return "Editor"; | |
case 3: return "Viewer"; | |
default: return "Unknown"; | |
}; | |
} | |
} |
In this post, Isha Fatima claims:
πͺπ΅π πΆπ π§π΅πΆπ ππ²πππ²πΏ?
- Faster Execution β Dictionary lookups are O(1), while if-else is O(n) in worst cases.
- Cleaner & Readable Code β The logic is more compact and easier to update.
- Scalability β If new roles are added, just update the dictionaryβno need to modify complex conditions!
Let's pick it apart:
Is it faster? Hell no! You can reason about this even without benchmarking it: The proposed solution is horrible because it allocates a dictionary each and every time the method is invoked. This involves memory allocation(s), building the dictionary (calculating hashes and the lookup table) each and every invocation.
Ok, but what if we make the dictionary static and assume Isha actually meant that? Well, then still each and every invocation a hash will have to be calculated to do the lookup. However, in the specific case of an int
the hash will likely be the int making it a very cheap hash. But that still doesn't make Isha's advice a good advice; for any other given type of lookup (e.g. string
) numbers will (wildly) vary. It's just not a good idea to make a general statement like this.
Cleaner and more readable code is pretty subjective in this particular case.
Scalability... I fail to see how adding another "if/else" to the code would be more 'complex' than the dictionary. Both read perfectly fine. However, the "better" approach doesn't provide any compile-time safety where one of the, often proposed in the comments, enum-variants of this implementation will. When these are used the compiler can help out and tell you that you need to forgot to add a case for a newly added value. That said, the "if-else-else" approach doesn't provide much compile-time safety either.
As we can see, the proposed method performs 5.52 times worse and allocates 216 bytes each invocation. The 'improved' static dictionary still performs 1.15 times worse than the baseline (so next time maybe run a benchmark before posting misinformation Isha?).
As you can see, actually the "if-else-else" performs best over all other alternatives!
For shits'n'giggles I ran the test with 64 values. Here are the results:
Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|
IfElse | 19.745 ns | 0.1915 ns | 0.1698 ns | 1.00 | - | NA |
SwitchExpression | 15.116 ns | 0.1985 ns | 0.1759 ns | 0.77 | - | NA |
SwitchStatement | 15.069 ns | 0.2882 ns | 0.2695 ns | 0.76 | - | NA |
Dictionary | 7.272 ns | 0.1470 ns | 0.1303 ns | 0.37 | - | NA |
The tests for above results and a lot more results and a graph can be found here.