Performance surprise with "as" and nullable types
Asked Answered
C

10

346

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);
    }
}
Ceyx answered 17/10, 2009 at 19:48 Comment(23)
Why not look at the jitted code? Even VS debugger can show it.Eshman
I'm just curious, did you test with CLR 4.0 as well?Malita
@Anton: Good point. Will do at some point (although this isn't in VS at the moment :) @divo: Yes, and it's worse all round. But then that's in beta, so there may be a lot of debugging code in there.Ceyx
Why not? VS debugger is happy to show JITted code in the Disassembly window, although it does not show real addresses or proper names for call targets (grrr!) Also, you could use windbg+SOS. One can even attach a windbg in read-only mode to a process being debugged in VS.Eshman
Re your comment; I also left some feedback with Karen re {Direct Comparisons}, "When the type argument is a nullable type, the comparison will do the obvious thing, treating an instance without a value as null." - as actually this behaviour isn't necessarily obvious, especially given the comments about non-nullable structs and the JIT handling.Paterfamilias
The Linq is not faster than the is-as above, you just have values that unconditionally executes even it is null: int? x = o as int?; try to move that inside of: if(x.HasValue) and change the if(x.HasValue) to: if(o is int). Then that is more apple-to-apple comparison. If you will modify as advised, is-as is faster than Linq. Lest be mistaken, I'm not the downvoterBourke
@Michael: No, the whole point of using "as" would be to only perform one operation which is dynamic based on the execution-time type. If x has a value then "if (o is int)" will definitely return true - but testing for x.HasValue should be very fast indeed; it's just fetching a field.Ceyx
@Jon: I mean, the As and Has code above suffers this kind of performance problem(some operation is unconditionally executed): i = 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, both as and HasValue are costly operations, they don't do in-memory-site operations, they have to make a staging variable first for the Has and Value magic to kick. Even Convert.ToInt32(o) is significantly faster than x.Value. I added some more testing on your code, kindly check the code belowBourke
@Jon: I have another benchmark regarding as and casting (albeit on reference type, if that matters). I posted a question regarding benchmarking, kindly take a look, thanks :-) #2680535Bourke
@Michael: The point is that performing the assignment unconditionally but then making the "if" fast should (IMO) be faster than performing the dynamic type check twice - at least in the case where a significant proportion of the values are integers. Assigning to a local variable should be very quick - it's not like it's got to allocate space for a new variable each time. Will look at your answer and other question soon (off to work now).Ceyx
I placed a bounty on this question to see if maybe someone out there knows the answer to this as I'm very curious, and also if this is still true with the new .Net 4.0 releaseAuscultate
@Earlz: Yes, it occurs with the released version too.Ceyx
Today I learnt you can use as on nullable types. Interesting, as it can't be used on other value types. Actually, more surprising.Chee
@Lepp it makes perfect sense for it to not work on value types. Think about it, as attempts to cast to a type and if it fails then it returns null. You can't set value types to nullAuscultate
Just for completeness: The idea, from one of the answers, to use if (o != null && o.GetType() == typeof(int)) { int x = (int)o; ... is nearly as fast as using is, and still much faster than "as nullable" or the LINQ way. I guess in this way we unbox the int twice. (When a type is sealed, like a struct, there's not much differece between an is check and a GetType() == something check.)Beeler
Perhaps worth mentioning: I tested it out right now and 'Cast' gives the exact same timing but 'As' is down to 800 and 'Linq' to 700 so there has been a vast improvement in the meantime. The conclusion still stands that it is about 6 times slower though.Bloated
@JeroenVannevel: Interesting - I'm still seeing it being ~13x slower. Probably depends on architecture etc.Ceyx
This version of Linq usage is simpler and faster values.Where(v => v is int).Cast<int>().Sum(), though still slower than 'Cast'Cassey
@RGudkov: You think Where(v => v is int).Cast<int>() is simpler than OfType<int>? I certainly don't.Ceyx
@JonSkeet I meant, it's simpler than 'Cast' and faster than your version of 'Linq'.Cassey
@RGudkov: I still don't understand - it looks like you're suggesting calling Where(...).Cast().Sum(). How is that simpler than calling Cast? And in what way is that simpler than OfType<int>().Sum()?Ceyx
@JonSkeet Your answer has 3 options: 'Cast' finishing in 121ms, 'As' in 2211ms and 'Linq' in 2143ms. So, by saying 'Cast', I mean first option from your answer. I suggested a 'New Linq' option - it is still a one-liner, hence simpler than 'Cast' option, but faster than 'Linq' option you tested. Hope it clarifies things.Cassey
@RGudkov: Ah, right. That wasn't clear to me. I'd still use the OfType<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
K
217

Clearly the machine code the JIT compiler can generate for the first case is much more efficient. One rule that really helps there is that an object can only be unboxed to a variable that has the same type as the boxed value. That allows the JIT compiler to generate very efficient code, no value conversions have to be considered.

The is operator test is easy, just check if the object isn't null and is of the expected type, takes but a few machine code instructions. The cast is also easy, the JIT compiler knows the location of the value bits in the object and uses them directly. No copying or conversion occurs, all machine code is inline and takes but about a dozen instructions. This needed to be really efficient back in .NET 1.0 when boxing was common.

Casting to int? takes a lot more work. The value representation of the boxed integer is not compatible with the memory layout of Nullable<int>. A conversion is required and the code is tricky due to possible boxed enum types. The JIT compiler generates a call to a CLR helper function named JIT_Unbox_Nullable to get the job done. This is a general purpose function for any value type, lots of code there to check types. And the value is copied. Hard to estimate the cost since this code is locked up inside mscorwks.dll, but hundreds of machine code instructions is likely.

The Linq OfType() extension method also uses the is operator and the cast. This is however a cast to a generic type. The JIT compiler generates a call to a helper function, JIT_Unbox() that can perform a cast to an arbitrary value type. I don't have a great explanation why it is as slow as the cast to Nullable<int>, given that less work ought to be necessary. I suspect that ngen.exe might cause trouble here.

Kennethkennett answered 19/6, 2010 at 17:28 Comment(1)
Okay, I'm convinced. I guess I'm used to thinking of "is" as potentially expensive because of the possibilities of walking up an inheritance hierarchy - but in the case of a value type, there's no possibility of a hierarchy, so it can be a simple bitwise comparison. I still think the JIT code for the nullable case could be optimised by the JIT a lot more heavily than it is though.Ceyx
M
27

It seems to me that the isinst is just really slow on nullable types. In method FindSumWithCast I changed

if (o is int)

to

if (o is int?)

which also significantly slows down execution. The only differenc in IL I can see is that

isinst     [mscorlib]System.Int32

gets changed to

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Malita answered 17/10, 2009 at 20:10 Comment(3)
It's more than that; in the "cast" case the isinst is followed by a test for nullity and then conditionally an unbox.any. In the nullable case there's an unconditional unbox.any.Ceyx
Yes, turns out both isinst and unbox.any are slower on nullable types.Malita
@Jon: You can review my answer as for why the cast is needed. (I know this is old, but I just discovered this q and thought I should provide my 2c of what I know about the CLR).Cero
C
22

This originally started out as a Comment to Hans Passant's excellent answer, but it got too long so I want to add a few bits here:

First, the C# as operator will emit an isinst IL instruction (so does the is operator). (Another interesting instruction is castclass, emited when you do a direct cast and the compiler knows that runtime checking cannot be ommited.)

Here is what isinst does (ECMA 335 Partition III, 4.6):

Format: isinst typeTok

typeTok is a metadata token (a typeref, typedef or typespec), indicating the desired class.

If typeTok is a non-nullable value type or a generic parameter type it is interpreted as “boxed” typeTok.

If typeTok is a nullable type, Nullable<T>, it is interpreted as “boxed” T

Most importantly:

If the actual type (not the verifier tracked type) of obj is verifier-assignable-to the type typeTok then isinst succeeds and obj (as result) is returned unchanged while verification tracks its type as typeTok. Unlike coercions (§1.6) and conversions (§3.27), isinst never changes the actual type of an object and preserves object identity (see Partition I).

So, the performance killer isn't isinst in this case, but the additional unbox.any. This wasn't clear from Hans' answer, as he looked at the JITed code only. In general, the C# compiler will emit an unbox.any after a isinst T? (but will omit it in case you do isinst T, when T is a reference type).

Why does it do that? isinst T? never has the effect that would have been obvious, i.e. you get back a T?. Instead, all these instructions ensure is that you have a "boxed T" that can be unboxed to T?. To get an actual T?, we still need to unbox our "boxed T" to T?, which is why the compiler emits an unbox.any after isinst. If you think about it, this makes sense because the "box format" for T? is just a "boxed T" and making castclass and isinst perform the unbox would be inconsistent.

Backing up Hans' finding with some information from the standard, here it goes:

(ECMA 335 Partition III, 4.33): unbox.any

When applied to the boxed form of a value type, the unbox.any instruction extracts the value contained within obj (of type O). (It is equivalent to unbox followed by ldobj.) When applied to a reference type, the unbox.any instruction has the same effect as castclass typeTok.

(ECMA 335 Partition III, 4.32): unbox

Typically, unbox simply computes the address of the value type that is already present inside of the boxed object. This approach is not possible when unboxing nullable value types. Because Nullable<T> values are converted to boxed Ts during the box operation, an implementation often must manufacture a new Nullable<T> on the heap and compute the address to the newly allocated object.

Cero answered 15/8, 2011 at 9:50 Comment(1)
I think the very last quoted sentence might have a typo; shouldn’t “...on the heap...” be “on the execution stack?” Seems like unboxing back into some new GC heap instance swaps the original problem for a nearly identical new one.Compartmentalize
P
19

Interestingly, I passed on feedback about operator support via dynamic being an order-of-magnitude slower for Nullable<T> (similar to this early test) - I suspect for very similar reasons.

Gotta love Nullable<T>. Another fun one is that even though the JIT spots (and removes) null for non-nullable structs, it borks it for Nullable<T>:

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
Paterfamilias answered 17/10, 2009 at 21:26 Comment(5)
Yowser. That's a really painful difference. Eek.Ceyx
If no other good has come out of all of this, it's led me to include warnings for both my original code and this :)Ceyx
I know this is an old question, but could you explain what you mean by "the JIT spots (and removes) null for non-nullable structs"? Do you mean it replaces null with a default value or something during runtime?Undertaker
@Justin - a generic method can be used at runtime with any number of permutations of generic-parameters (T etc). The stack etc requirements depend on the args (amount of stack space for a local, etc), so you get one JIT for any unique permutation involving a value type. However, references are all the same size so share a JIT. While doing the per-value-type JIT, it can check for a few obvious scenarios, and tries to excise unreachable code due to things like impossible nulls. It isn't perfect, note. Also, I'm ignoring AOT for the above.Paterfamilias
The unrestricted nullable test is still 2.5 orders of magnitude slower, but there's some optimisation going on when you don't use the count variable. Adding Console.Write(count.ToString()+" "); after the watch.Stop(); in both cases slows the other tests by just under an order of magnitude, but the unrestricted nullable test is not changed. Note there are also changes when you test the cases when null is passed, confirming the original code isn't really doing the null check and increment for the other tests. LinqpadLeatherworker
C
15

In order to keep this answer up-to-date, it's worth mentioning that the most of the discussion on this page is now moot now with C# 7.1 and .NET 4.7 which supports a slim syntax that also produces the best IL code.

The OP's original example...

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

becomes simply...

if (o is int x)
{
    // ...use x in here
}

I have found that one common use for the new syntax is when you are writing a .NET value type (i.e. struct in C#) that implements IEquatable<MyStruct> (as most should). After implementing the strongly-typed Equals(MyStruct other) method, you can now gracefully redirect the untyped Equals(Object obj) override (inherited from Object) to it as follows:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


Appendix: The Release build IL code for the first two example functions shown above in this answer (respectively) are given here. While the IL code for the new syntax is indeed 1 byte smaller, it mostly wins big by making zero calls (vs. two) and avoiding the unbox operation altogether when possible.

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

For further testing which substantiates my remark about the performance of the new C#7 syntax surpassing the previously available options, see here (in particular, example 'D').

Compartmentalize answered 21/8, 2017 at 1:30 Comment(0)
B
12

This is the result of FindSumWithAsAndHas above: alt text

This is the result of FindSumWithCast: alt text

Findings:

  • Using as, it test first if an object is an instance of Int32; under the hood it is using isinst Int32 (which is similar to hand-written code: if (o is int) ). And using as, it also unconditionally unbox the object. And it's a real performance-killer to call a property(it's still a function under the hood), IL_0027

  • Using cast, you test first if object is an int if (o is int); under the hood this is using isinst Int32. If it is an instance of int, then you can safely unbox the value, IL_002D

Simply put, this is the pseudo-code of using as approach:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

And this is the pseudo-code of using cast approach:

if (o isinst Int32)
    sum += (o unbox Int32)

So the cast ((int)a[i], well the syntax looks like a cast, but it's actually unboxing, cast and unboxing share the same syntax, next time I'll be pedantic with the right terminology) approach is really faster, you only needed to unbox a value when an object is decidedly an int. The same thing can't be said to using an as approach.

Bourke answered 19/6, 2010 at 14:4 Comment(0)
B
9

Profiling further:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

Output:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

What can we infer from these figures?

  • First, is-then-cast approach is significantly faster than as approach. 303 vs 3524
  • Second, .Value is marginally slower than casting. 3524 vs 3272
  • Third, .HasValue is marginally slower than using manual has(i.e. using is). 3524 vs 3282
  • Fourth, doing an apple-to-apple comparison(i.e. both assigning of simulated HasValue and converting simulated Value happens together) between simulated as and real as approach, we can see simulated as is still significantly faster than real as. 395 vs 3524
  • Lastly, based on first and fourth conclusion, there's something wrong with as implementation ^_^
Bourke answered 17/10, 2009 at 19:49 Comment(0)
V
8

I don't have time to try it, but you may want to have:

foreach (object o in values)
        {
            int? x = o as int?;

as

int? x;
foreach (object o in values)
        {
            x = o as int?;

You are creating a new object each time, which won't completely explain the problem, but may contribute.

Vookles answered 17/10, 2009 at 19:59 Comment(7)
No, I ran that and it is marginally slower.Entomostracan
Declaring a variable in a different place only affects the generated code significantly when the variable is captured (at which point it affects the actual semantics) in my experience. Note that it's not creating a new object on the heap, although it's certainly creating a new instance of int? on the stack using unbox.any. I suspect that's the issue - my guess is that hand-crafted IL could beat both options here... although it's also possible that the JIT is optimised to recognise for the is/cast case and only check once.Ceyx
I was thinking that the cast is probably optimized since it has been around for so long.Vookles
is/cast is an easy target for optimization, it's such an annoyingly common idiom.Eshman
@James Black: The cast has definitely been optimized, as conversion has been significantly faster (in .NET 1.1 if I remember correctly) as this no longer up-to-date benchmark shows: codeproject.com/KB/cs/csharpcasts.aspx.Malita
Local variables are allocated on the stack when the stack frame for the method is created, so where you declare the variable in the method makes no difference at all. (Unless it's in a closure of course, but that's not the case here.)Breann
The jitter automatically uses the same variable in these cases (when it's not captured by a lambda or anything). This has been confirmed by Eric Lippert.Gemination
D
8

I tried the exact type check construct

typeof(int) == item.GetType(), which performs as fast as the item is int version, and always returns the number (emphasis: even if you wrote a Nullable<int> to the array, you would need to use typeof(int)). You also need an additional null != item check here.

However

typeof(int?) == item.GetType() stays fast (in contrast to item is int?), but always returns false.

The typeof-construct is in my eyes the fastest way for exact type checking, as it uses the RuntimeTypeHandle. Since the exact types in this case don't match with nullable, my guess is, is/as have to do additional heavylifting here on ensuring that it is in fact an instance of a Nullable type.

And honestly: what does your is Nullable<xxx> plus HasValue buy you? Nothing. You can always go directly to the underlying (value) type (in this case). You either get the value or "no, not an instance of the type you were asking for". Even if you wrote (int?)null to the array, the type check will return false.

Delegate answered 19/6, 2010 at 10:1 Comment(1)
Interesting... the idea of using the "as" + HasValue (not is plus HasValue, note) is that it's only performing the type check once instead of twice. It's doing the "check and unbox" in a single step. That feels like it should be faster... but it clearly isn't. I'm not sure what you mean by the last sentence, but there's no such thing as a boxed int? - if you box an int? value it ends up as a boxed int or a null reference.Ceyx
B
7
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);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {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);
    }
}

Outputs:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[EDIT: 2010-06-19]

Note: Previous test was done inside VS, configuration debug, using VS2009, using Core i7(company development machine).

The following was done on my machine using Core 2 Duo, using VS2010

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
Bourke answered 21/4, 2010 at 0:28 Comment(5)
Which framework version are you using, out of interest? The results on my netbook (using .NET 4RC) are even more dramatic - the versions using As are much worse than your results. Maybe they've improved it for .NET 4 RTM? I still think it could be faster...Ceyx
@Michael: Were you running an unoptimised build, or running in the debugger?Ceyx
@Jon: unoptimized build, under debuggerBourke
@Michael: Right - I tend to view performance results under a debugger as largely irrelevant :)Ceyx
@Jon: If by under debugger, meaning inside of VS; yes the previous benchmark was done under debugger. I benchmark again, inside of VS and outside of it, and compiled as debug and compiled as release. Check the editBourke

© 2022 - 2024 — McMap. All rights reserved.