"using static" kills AsParallel
Asked Answered
K

1

11

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

Kingcraft answered 30/1, 2021 at 19:56 Comment(6)
The correct syntax is using static <fully-qualified-type-name>, so using static System.Linq.Enumerable;Nidanidaros
@MikeTsayper Your conclusion is correct. The compiled code changes.Blanket
@AluanHaddad on my pc it runs for about 600 ms uncommented, 200 commented, output sequential/shuffledKingcraft
@Nidanidaros even with the fully qualified name, the problem persistsKingcraft
It appears that "ToArray" binds to different extension methods when commenting/uncommenting the using static. I'm made a simpler repro, and reported to Roslyn team for investigation (it's not necessarily a bug, I'm not sure).Blanket
@Blanket Good find. I would however expect that to be "working as designed".Stuffing
N
16

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.

Nidanidaros answered 30/1, 2021 at 20:21 Comment(5)
Is it fair to boil this down and say this is by design then? I see nothing in the documentation about how the compiler picks extension methods where there's more than one that fits the bill. Your answer makes sense to me, just would love to see where in the C# spec or documentation this is made clear.Gunsmith
@Gunsmith Try hereCanova
@Gunsmith expanded the response... check if it is clearNidanidaros
Crystal clear. Thanks for expanding. Very useful for those like me that avoid extension methods (for performance reasons that are insignificant 99.9% of the time).Gunsmith
@Gunsmith What would be the performance downsides of extension methods per se? Or do you mean specifically the LINQ ones?Canova

© 2022 - 2024 — McMap. All rights reserved.