Search code examples
c#plinq

"using static" kills AsParallel


In the following code, if you uncomment the "using static" line, the query will not run in parallel. Why?

(Visual Studio Community 2019, .Net Core 3.1 / .Net 4.8)

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace UsingStatic_Mystery
{
    //using static Enumerable;
    class Program
    {
        static void Main(string[] args)
        {
            var w = new Stopwatch();
            iter:
            w.Start();
            var xx = Enumerable.Range(0, 10)
                .AsParallel()
                .OrderByDescending(x => {
                    Thread.Sleep(new Random().Next(100));
                    Console.WriteLine(x);
                    return x;
                }).ToArray();
            w.Stop();
            Console.WriteLine();
            foreach (var x in xx) Console.WriteLine(x);
            Console.WriteLine(w.ElapsedMilliseconds);
            Console.ReadLine();
            w.Reset();
            goto iter;
        }
    }
}

output, uncommented/commented:

"using static" uncommented "using static" commented


Solution

  • Found:

    This is the IL code generated with the using static commented (so no using static):

    IL_0038: call class [System.Linq.Parallel]System.Linq.OrderedParallelQuery`1<!!0> [System.Linq.Parallel]System.Linq.ParallelEnumerable::OrderByDescending<int32, int32>(class [System.Linq.Parallel]System.Linq.ParallelQuery`1<!!0>, class [System.Private.CoreLib]System.Func`2<!!0, !!1>)
    IL_003d: call !!0[] [System.Linq.Parallel]System.Linq.ParallelEnumerable::ToArray<int32>(class [System.Linq.Parallel]System.Linq.ParallelQuery`1<!!0>)
    

    and this is the IL code generated with the using static uncommented (so with using static):

    IL_0038: call class [System.Linq]System.Linq.IOrderedEnumerable`1<!!0> [System.Linq]System.Linq.Enumerable::OrderByDescending<int32, int32>(class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<!!0>, class [System.Private.CoreLib]System.Func`2<!!0, !!1>)
    IL_003d: call !!0[] [System.Linq]System.Linq.Enumerable::ToArray<int32>(class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<!!0>)
    

    The "correct" side is using Parallel.OrderBy, the "wrong" side is using Enumerable.OrderBy. The result you see is quite clearly for this reason. And the reason for why one or the other OrderBy is selected is because with the using static Enumerable you declare that the C# should prefer methods in the Enumerable class.

    More interestingly, had you written the using block like this:

    using System;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading;
    
    using static System.Linq.Enumerable;
    
    namespace ConsoleApp1
    {
    

    so outside the namespace, everything would have worked "correctly" (IL code generated).

    I'll say that namespace resolution works by level... First C# tries all the using defined in the innermost level of namespace, if there is no one that is good enough then it goes up a level of namespace. If there are multiple candidates in the same level it takes the best match. In the example without the using static and the example I gave where the using + the using static are all top-level, there is a single level, so the C# takes the best candidate. In the two-levels using the innermost one is checked, and the using static Enumerable is good enough to resolve the OrderBy method, so no extra checking is done.

    I'll say that this time again, SharpLab was the MVP of this response. If you have a question about what the C# compiler does under the hood, SharpLab can give you the response (technically you could use ildasm.exe or ILSpy, but SharpLab is very immediate because it is a web site, and you can interactively change the source code). The SVP (second valuable player) (for me) was WinMerge, that I used to compare the IL assemblies 😀

    Answer to the comment

    The C# 6.0 draft reference page says

    The namespace_name referenced by a using_namespace_directive is resolved in the same way as the namespace_or_type_name referenced by a using_alias_directive. Thus, using_namespace_directives in the same compilation unit or namespace body do not affect each other and can be written in any order.

    and then

    Ambiguities between multiple using_namespace_directives and using_static_directives are discussed in Using namespace directives.

    so the first rule is applied even to using static. This explains why the third example (mine) is equivalent to the no-using static.

    About why ParallelEnumerable.OrderedBy() is better than Enumerable.OrderBy() when both of them are checked by the C# compiler, it is simple:

    The AsParallel() returns a ParallelQuery<TSource> (that implements IEnumerable<TSource>)

    The ParallelEnumerable.OrderedBy() signature:

    public static OrderedParallelQuery<TSource> OrderBy<TSource, TKey>(this ParallelQuery<TSource> source, Func<TSource, TKey> keySelector)
    

    The Enumerable.OrderedBy() signature:

    public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    

    The first one accepts a ParallelQuery<TSource>, that is the exact same type returned by AsParallel(), no "downcast" necessary.