We all cast. Unfortunately, I’m not referring to spells. That would be awesome, of course, but I’m talking about programming and casting from one type to another. Have you ever thought about the differences between them? You most likely did. However, have you measured their speed? I'm willing to bet you didn't. If so, you've come to the right place, because that is exactly what we intend to do. Without further ado, Let's get started.
Infrastructure
To test and to benchmark we need something to work with. None of you will be surprised when I tell you that we are going to work with a simple class. Just like that one.
1 2 3 4 5 6
public class BlogData { public Guid Id { get; set; } public string Title { get; set; } public DateTime PublicationDate { get; set; } }
As you can see, class is nothing out of the ordinary. Only a few fields are required to give us something to work with. We need a provider class now that we have a POCO class. When it comes to benchmarking operations on objects, this is the preferred method. Because benchmark requires object initialization. This is how the provider class should look:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
public class ObjectsProvider { public static object BlogData = new BlogData() { Id = Guid.NewGuid(), Title = "Magic of Casting", PublicationDate = DateTime.Now }; public static object NotBlogData = new { Id = Guid.NewGuid(), Title = "Magic of Casting", PublicationDate = DateTime.Now }; public static List<object> BlogDataList = Enumerable.Range(0, 1000) .Select(x => (object)new BlogData() { Id = Guid.NewGuid(), Title = Guid.NewGuid().ToString(), PublicationDate = DateTime.Now }).ToList(); }
Provider has three properties. BlogData – is an object type fields that holds an instance on BlogData class. NotBlogData - is an object type fields that holds an anonymous object. BlogDataList - a list of object types that are instances of BlogData.
Casting options
Before we begin benchmarking, we must discuss casting options. The first option is a hard cast. The code as well as the result look like this:
1 2
BlogData bd = (BlogData)ObjectsProvider.BlogData; Console.WriteLine(bd.Title);
This is the simplest way of casting and when an object can be cast, everything works perfectly – but let's see what happens if the object cannot be cast.
1 2
BlogData bd = (BlogData)ObjectsProvider.NotBlogData; Console.WriteLine(bd.Title);
In this case, we get an InvalidCastException. When codding in this manner, we must keep this in mind.
The safe cast option comes in second. It employs the "as" keyword and reads as follows.
1 2
BlogData? bd = ObjectsProvider.BlogData as BlogData; Console.WriteLine(bd.Title);
If we try to safe cast an object that can’t be casted the resulted object will be a null. This also needs to be considered when using this kind of casting. If the casting is possible, there will be no problem.
1 2
BlogData? bd = ObjectsProvider.NotBlogData as BlogData; Console.WriteLine(bd.Title);
Last but not least, we can consider the match cast. In this case, we check to see if something is of a type, and if it is, the result of successful casting is stored in a variable, and the if statement follows. Option that is both elegant and readable.
1 2
if(ObjectsProvider.BlogData is BlogData bd) Console.WriteLine(bd.Title);
Benchmarking Round 1 – Single Object
Now that we've seen all of the basic casting approaches, it's time to see how they perform. Benchmarking, as we all know, is the process of comparing the performance of various solutions. In this case, we'll be utilising an excellent Nuget Package called BenchmarkDotNet. The documentation and code for the solution can be found here. I strongly advise you to familiarise yourself with it.
We need a code to benchmark now that we have the tool. In this case, we want to compare the various options for casting a single object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
using BenchmarkDotNet.Attributes; namespace Emample3; [MemoryDiagnoser()] public class BanchmarkCastMethods { [Benchmark] public BlogData GetByHardCast() { BlogData bd = (BlogData)ObjectsProvider.BlogData; return bd; } [Benchmark] public BlogData GetBySafeCast() { BlogData? bd = ObjectsProvider.BlogData as BlogData; return bd; } [Benchmark] public BlogData GetByMatchCast() { if (ObjectsProvider.NotBlogData is BlogData bd) return bd; return null!; } }
As you can see, the only work that needed to be done was to incorporate the code for successful casting options into methods and to add benchmarking arguments. To run this code, switch the application to Release mode and execute it.
BenchmarkRunner.Run<BanchmarkCastMethods>();
After a few minutes, around 2-3 in my case, the results are back
They're intriguing but not particularly surprising. We probably all had a gut feeling that hard cast would be the quickest. So there's no surprise there. What surprised me was the magnitude of how much faster it is. We're talking 22 and 28 times faster, respectively. That is very impressive and should be taken into account when coding.
The Challenge
Finally, the fun begins. Because when is came to a single object the casting options are straight forward rally. With the collection, not so much. I’m sure you remember that inside our objects provides there in an option to get collection of objects.
1 2 3 4 5 6 7 8
public static List<object> BlogDataList = Enumerable.Range(0, 1000) .Select(x => (object)new BlogData() { Id = Guid.NewGuid(), Title = Guid.NewGuid().ToString(), PublicationDate = DateTime.Now }).ToList();
I have a challenge for you right now: write a method that takes this Listobject> and returns the ListBlogData>. Make it as fast as possible. When you've done that, keep reading and compare your method to the ones that will be presented.
Benchmarking Round 2 – Collections of objects
If you haven't already, please do so now that you've created your method. I'll show you the options we'll be considering. After you've finished looking, run a BenchmarkListCastMethods benchmark.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
using BenchmarkDotNet.Attributes; namespace Emample3; [MemoryDiagnoser()] public class BenchmarkListCastMethods { [Benchmark()] public List<BlogData> GetByOfPeople() => ObjectsProvider.BlogDataList .OfType<BlogData>() .ToList(); [Benchmark()] public List<BlogData> GetByCastAs() => ObjectsProvider.BlogDataList .Where(x => x as BlogData is not null) .Cast<BlogData>() .ToList(); [Benchmark()] public List<BlogData> GetByCastIs() => ObjectsProvider.BlogDataList .Where(x => x is BlogData) .Cast<BlogData>() .ToList(); [Benchmark()] public List<BlogData> GetByHardAs() => ObjectsProvider.BlogDataList .Where(x => x as BlogData is not null) .Select(x => (BlogData)x) .ToList(); [Benchmark()] public List<BlogData> GetByHardIs() => ObjectsProvider.BlogDataList .Where(x => x is BlogData) .Select(x => (BlogData)x) .ToList(); [Benchmark()] public List<BlogData> GetByHardTypeOf() => ObjectsProvider.BlogDataList .Where(x => x.GetType() == typeof(BlogData)) .Select(x => (BlogData)x) .ToList(); }
The results are here, and they are surprising. There are two things worth noticing. One, the difference are not so vast. The slowest time is 27 microseconds, and the fastest time is 10 microseconds. What was the quickest method is more interesting. GetByHardTypeOf was the quickest. We used the formula x => x. GetType() == typeof(BlogData)) to determine whether an object can be casted and hard casting to perform actual casting.
Summary
We have just looked into the majority of the casting possibilities, and the distinctions are clearly visible. Please keep them in mind as you code, as they might turn out to be useful to you.
If you have any questions, please drop me a line at karol.rogowski@softwarehut.com.
Till the next time. Keep coding.