我是 profiling我们的 C# .NET 应用程序,我注意到方法 System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start
出现多次,占用了我 1 分钟样本的大约 3-4 秒的自拍时间(这意味着它在任务基础结构中花费了大约 3-4 秒)。
据我了解,编译器使用此方法来实现 async
/await
C# 中的语言构造。通常,其中有什么会导致它阻塞或以其他方式占用大量时间?有没有什么方法可以改进我们的方法,以减少在此基础设施上花费的时间?
编辑:这是一个有点冗长但仍然独立的代码示例,演示了这个问题,本质上是在两个非常大的数组上进行并行合并排序:
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;
}
}
}
}
在此代码的性能配置文件中,System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start
的两个副本(每个通用值类型一个)占用大部分自处理器时间,两个 MergeSort
方法只占用自处理器时间的一小部分。 Task.Run
时也注意到了类似的行为未使用(因此仅使用单个处理器)。
编辑 2:非常感谢您到目前为止的回答。我原本以为 Task<TResult>
的事实被使用是问题的一部分(因为它在原始代码中被使用),因此我的结构是复制数组而不是就地排序。但是,我现在认识到那是无关紧要的,所以我更改了上面的代码片段,改为在适当的位置进行合并排序。我还通过引入一个非平凡的顺序截止(为了严格限制时间而进行选择排序)以及使用 Comparer
来 reduce task 数。对象以防止数组元素的装箱分配(从而减少垃圾收集器造成的分析干扰)。
但是,相同的模式,AsyncTaskMethodBuilder::Start
的模式花费大量的 self 时间,仍然存在并且仍然可以在分析结果中找到。
编辑 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;
这已经是个问题了。请记住,每个 await
都很重要。如果Task
已经完成的事件,await
此时仍然拆分函数,释放当前的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];
这样您就可以减少函数中的任务切换次数。
其次:减少创建的Task
实例的数量。
任务是在不同 CPU 核心上分配工作的好工具,但您必须确保它们很忙。创建一个新的 Task
是有难度的,您必须确保它是值得的。
我建议在创建新 Task
的点上添加一个阈值。如果您正在处理的部分变得太小,则不应创建新的 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);
}
您应该尝试不同的值,看看这会如何改变。 100
只是我开始时使用的一个值,但您可以选择任何其他值。这将大大减少没有多少工作要做的任务。基本上,该值决定了要处理的一项任务可接受的剩余工作量。
最后,您应该考虑以不同方式处理您的 array
实例。如果您告诉您的函数它们期望在其中工作的数组部分的起始位置和长度,您应该能够进一步提高性能,因为您不必将数组复制数千次。
关于c# - System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start 有大量的自用时间,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44463407/