System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start 有大量的自我时间

The*_*tor 8 c# profiling async-await

我正在分析我们的 C# .NET 应用程序,我注意到该方法System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start多次出现,占用了我的 1 分钟示例中大约 3-4 秒的 Self Time(这意味着它花费了大约 3-4 秒)在任务基础结构中)。

我知道编译器使用此方法来实现C# 中的async/await语言构造。一般来说,里面有什么会导致它阻塞或以其他方式占用大量时间?有什么方法可以改进我们的方法,让它在这个基础设施上花费更少的时间?

编辑:这是一个有点冗长但仍然独立的代码示例来演示这个问题,本质上是对两个非常大的数组进行并行合并排序:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncAwaitSelfTimeTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Random random = new Random();

            int[] arrayOne = GenerateArray(50_000_000, random.Next);
            double[] arrayTwo = GenerateArray(50_000_000, random.NextDouble);

            Comparer<int> comparerOne = Comparer<int>.Create((a, b) =>
            {
                if (a < b) return -1;
                else if (a > b) return 1;
                else return 0;
            });
            Comparer<double> comparerTwo = Comparer<double>.Create((a, b) =>
            {
                if (a < b) return -1;
                else if (a > b) return 1;
                else return 0;
            });

            var sortTaskOne = Task.Run(() => MergeSort(arrayOne, 0, arrayOne.Length, comparerOne));
            var sortTaskTwo = Task.Run(() => MergeSort(arrayTwo, 0, arrayTwo.Length, comparerTwo));

            Task.WaitAll(sortTaskOne, sortTaskTwo);
            Console.Write("done sorting");
        }

        static T[] GenerateArray<T>(int length, Func<T> getFunc)
        {
            T[] result = new T[length];
            for (int i = 0; i < length; i++)
            {
                result[i] = getFunc();
            }
            return result;
        }

        static async Task MergeSort<T>(T[] array, int start, int end, Comparer<T> comparer)
        {
            if (end - start <= 16)
            {
                SelectionSort(array, start, end, comparer);
            }
            else
            {
                int mid = start + (end - start) / 2;

                Task firstTask = Task.Run(() => MergeSort(array, start, mid, comparer));
                Task secondTask = Task.Run(() => MergeSort(array, mid, end, comparer));

                await Task.WhenAll(firstTask, secondTask);

                int firstIndex = start;
                int secondIndex = mid;
                T[] dest = new T[end - start];
                for (int i = 0; i < dest.Length; i++)
                {
                    if (firstIndex >= mid)
                    {
                        dest[i] = array[secondIndex++];
                    }
                    else if (secondIndex >= end)
                    {
                        dest[i] = array[firstIndex++];
                    }
                    else if (comparer.Compare(array[firstIndex], array[secondIndex]) < 0)
                    {
                        dest[i] = array[firstIndex++];
                    }
                    else
                    {
                        dest[i] = array[secondIndex++];
                    }
                }

                dest.CopyTo(array, start);
            }
        }

        static void SelectionSort<T>(T[] array, int start, int end, Comparer<T> comparer)
        {
            // note: using selection sort here to prevent time variability
            for (int i = start; i < end; i++)
            {
                int minIndex = i;
                for (int j = i + 1; j < end; j++)
                {
                    if (comparer.Compare(array[j], array[minIndex]) < 0)
                    {
                        minIndex = j;
                    }
                }
                T temp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = temp;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这段代码的性能配置文件中,两个副本System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start(每个泛型值类型一个)占用了大部分自处理器时间,而这两种MergeSort方法只占用了自处理器时间的很小一部分。Task.Run未使用时也出现了类似的行为(因此仅使用单个处理器)。

编辑 2:非常感谢您到目前为止的回答。我最初认为Task<TResult>正在使用的事实是问题的一部分(因为它在原始代码中使用),因此我的结构是复制数组而不是原地排序。但是,我现在意识到这是无关紧要的,所以我更改了上面的代码片段,改为在适当的位置进行合并排序。我还通过引入非平凡的顺序截止(为了严格限制时间而进行选择排序)以及使用Comparer对象来防止数组元素的装箱分配(从而减少由垃圾收集器)。

然而,同样的模式,即AsyncTaskMethodBuilder::Start占用大量自我时间,仍然存在,并且仍然可以在分析结果中找到。

编辑 3:澄清一下,我正在/正在寻找的答案不是“为什么这段代码很慢?”,而是“为什么 .NET 探查器告诉我大部分成本都花在了我无法控制的方法中?” 接受的答案帮助我确定了问题,即大部分逻辑都在分析器不包括的生成类型中。

Nit*_*ram 5

您在这里遇到的问题是,您产生了许多使正常任务池过载的任务,从而导致 .NET 产生额外的任务。由于您一直在创建新任务,直到数组的长度为1. AsyncTaskMethodBuilder::Start一旦它需要创建新任务以继续执行并且无法重用池中的任务,就开始成为一个重要的时间消耗者。

您需要更改一些内容才能使您的函数获得一些性能:

第一:清理你await

Task<T[]> firstTask = Task.Run(() => MergeSort(firstHalf));
Task<T[]> secondTask = Task.Run(() => MergeSort(secondHalf));

await Task.WhenAll(firstTask, secondTask);

T[] firstDest = await firstTask;
T[] secondDest = await secondTask;
Run Code Online (Sandbox Code Playgroud)

这已经是个问题了。请记住,每一个都很await重要。事件如果Task已经完成,此时await仍然拆分函数,释放当前Task并在新的Task. 这个切换需要时间。不多,但这种情况在您的职能中经常发生,而且是可以衡量的。

Task.WhenAll 已经返回您需要的结果值。

Task<T[]> firstTask = Task.Run(() => MergeSort(firstHalf));
Task<T[]> secondTask = Task.Run(() => MergeSort(secondHalf));

T[][] dests = await Task.WhenAll(firstTask, secondTask);

T[] firstDest = dests[0];
T[] secondDest = dests[1];
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以减少函数中的任务切换数量。

第二:减少Task创建的实例数量。

任务是在不同 CPU 内核上分配工作的好工具,但您必须确保它们很忙。创造一个新事物是有好处的Task,你必须确保它是值得的。

我建议Task在创建new的地方添加一个阈值。如果您正在处理的部分太小,则不应创建新Task实例,而是直接调用函数。

例如:

T[] firstDest;
T[] secondDest;
if (mid > 100) 
{
  Task<T[]> firstTask = Task.Run(() => MergeSort(firstHalf));
  Task<T[]> secondTask = Task.Run(() => MergeSort(secondHalf));

  T[][] dests = await Task.WhenAll(firstTask, secondTask);

  firstDest = dests[0];
  secondDest = dests[1];
} 
else 
{
  firstDest = MergeSort(firstHalf);
  secondDest = MergeSort(secondHalf);
}
Run Code Online (Sandbox Code Playgroud)

你应该尝试不同的值,看看这会如何改变事情。100只是我要开始的一个值,但您可以选择任何其他值。这将大量减少没有太多工作要做的任务量。基本上,该值决定了要处理的一项任务可接受的剩余工作量。

最后,您应该考虑以array不同方式处理您的实例。如果你告诉你的函数开始位置和它们预期工作的数组部分的长度,你应该能够进一步提高性能,因为你不必复制数组数千次。


nos*_*tio 2

有趣的。我获取了您的示例并将MergeSort异步方法更改为非异步。现在,分析会话需要大约 33 秒才能完成(异步版本大约需要 36 秒,两者都使用发布配置)。非异步版本如下所示:

    static Task<T[]> MergeSort<T>(T[] src) where T : IComparable<T>
    {
        if (src.Length <= 1)
        {
            return Task.FromResult(src);
        }
        else
        {
            int mid = src.Length / 2;
            T[] firstHalf = new T[mid];
            T[] secondHalf = new T[src.Length - mid];
            Array.Copy(src, 0, firstHalf, 0, mid);
            Array.Copy(src, mid, secondHalf, 0, src.Length - mid);

            Task<T[]> firstTask = Task.Run(() => MergeSort(firstHalf));
            Task<T[]> secondTask = Task.Run(() => MergeSort(secondHalf));

            return Task.WhenAll(firstTask, secondTask).ContinueWith(
                continuationFunction: _ =>
                {
                    T[] firstDest = firstTask.Result;
                    T[] secondDest = secondTask.Result;
                    int firstIndex = 0;
                    int secondIndex = 0;

                    T[] dest = new T[src.Length];
                    for (int i = 0; i < dest.Length; i++)
                    {
                        if (firstIndex >= firstDest.Length)
                        {
                            dest[i] = secondDest[secondIndex++];
                        }
                        else if (secondIndex >= secondDest.Length)
                        {
                            dest[i] = firstDest[firstIndex++];
                        }
                        else if (firstDest[firstIndex].CompareTo(secondDest[secondIndex]) < 0)
                        {
                            dest[i] = firstDest[firstIndex++];
                        }
                        else
                        {
                            dest[i] = secondDest[secondIndex++];
                        }
                    }

                    return dest;
                },
                cancellationToken: System.Threading.CancellationToken.None,
                continuationOptions: TaskContinuationOptions.ExecuteSynchronously,
                scheduler: TaskScheduler.Default);
        }
    }
Run Code Online (Sandbox Code Playgroud)

因此,对于这个特定的示例,异步/等待开销似乎约为 3 秒。这超出了我的预期,但这肯定不是瓶颈。

关于这一观察:

在此代码的性能分析中,System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start 的两个副本(每个泛型值类型各一个)占用了大部分自处理器时间,而两个 MergeSort 方法只占用了非常多的时间。自处理器时间的一小部分。

我还没有分析这个特定异步方法的编译器生成的代码,但我怀疑MergeSort只包含一个简短的序言/结尾代码,而实际的 CPU 密集型代码是由AsyncTaskMethodBuilder::Start.

  • 正如您所怀疑的,编译后的“MergeSort”方法确实只包含一个简短的序言/尾声 - 实际代码在生成的内部类型中找到。我不确定为什么 `AsyncTaskMethodBuilder::Start` 将其所有执行时间报告为自身时间(因为此类存在),但您的解释似乎是合理的。 (3认同)