I'm just revising chapter 4 of C# in Depth which deals with nullable types, and I'm adding a section about using the "as" operator, which allows you to write:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
I thought this was really neat, and that it could improve performance over the C# 1 equivalent, using "is" followed by a cast - after all, this way we only need to ask for dynamic type checking once, and then a simple value check.
This appears not to be the case, however. I've included a sample test app below, which basically sums all the integers within an object array - but the array contains a lot of null references and string references as well as boxed integers. The benchmark measures the code you'd have to use in C# 1, the code using the "as" operator, and just for kicks a LINQ solution. To my astonishment, the C# 1 code is 20 times faster in this case - and even the LINQ code (which I'd have expected to be slower, given the iterators involved) beats the "as" code.
Is the .NET implementation of isinst
for nullable types just really slow? Is it the additional unbox.any
that causes the problem? Is there another explanation for this? At the moment it feels like I'm going to have to include a warning against using this in performance sensitive situations...
Results:
Cast: 10000000 : 121
As: 10000000 : 2211
LINQ: 10000000 : 2143
Code:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
x.HasValue
should be very fast indeed; it's just fetching a field. – Ceyxi = someValue; if (a > 10) i = 7;
should be:if (a > 10) i = 7; else i = someValue;
, that's what i mean by apple-to-apple comparison. I think on value types, bothas
andHasValue
are costly operations, they don't doin-memory-site
operations, they have to make a staging variable first for theHas
andValue
magic to kick. EvenConvert.ToInt32(o)
is significantly faster thanx.Value
. I added some more testing on your code, kindly check the code below – Bourkeas
and casting (albeit on reference type, if that matters). I posted a question regarding benchmarking, kindly take a look, thanks :-) #2680535 – Bourkeas
on nullable types. Interesting, as it can't be used on other value types. Actually, more surprising. – Cheeas
attempts to cast to a type and if it fails then it returns null. You can't set value types to null – Auscultateif (o != null && o.GetType() == typeof(int)) { int x = (int)o; ...
is nearly as fast as usingis
, and still much faster than "as
nullable" or the LINQ way. I guess in this way we unbox theint
twice. (When a type is sealed, like a struct, there's not much differece between anis
check and aGetType() == something
check.) – Beelervalues.Where(v => v is int).Cast<int>().Sum()
, though still slower than 'Cast' – CasseyWhere(v => v is int).Cast<int>()
is simpler thanOfType<int>
? I certainly don't. – CeyxWhere(...).Cast().Sum()
. How is that simpler than calling Cast? And in what way is that simpler thanOfType<int>().Sum()
? – CeyxOfType<int>().Sum()
personally - I think that's the clearest code... I'd only optimize to something else when I knew it was performance critical. Of course, it's possible that all of this has changed, with .NET Core etc. – Ceyx