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 探查器告诉我大部分成本都花在了我无法控制的方法中?” 接受的答案帮助我确定了问题,即大部分逻辑都在分析器不包括的生成类型中。
您在这里遇到的问题是,您产生了许多使正常任务池过载的任务,从而导致 .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不同方式处理您的实例。如果你告诉你的函数开始位置和它们预期工作的数组部分的长度,你应该能够进一步提高性能,因为你不必复制数组数千次。
有趣的。我获取了您的示例并将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.
| 归档时间: |
|
| 查看次数: |
8377 次 |
| 最近记录: |