Тесты показывают, что "ожидание" значительно медленнее, даже если ожидаемый объект уже завершен
Я хотел протестировать накладные расходы, приписываемые программе, используя await/async.
Чтобы проверить это, я написал следующий тестовый класс:
public class Entity : INotifyCompletion {
private Action continuation;
private int i;
public void OnCompleted(Action continuation) {
this.continuation = continuation;
}
public Entity GetAwaiter() {
return this;
}
public Entity GetResult() {
return this;
}
public bool IsCompleted { get { return true; } }
public void Execute() {
if (i > 0) Console.WriteLine("What");
}
}
И затем я написал тестовую упряжь. Испытательный жгут проходит через TestA и TestB 1600 раз, измеряя последние только 1500 раз (чтобы JIT мог "разогреться" ). set
- это набор объектов Entity (но реализация не имеет значения). В комплекте установлено 50 000 объектов. Испытательный жгут использует класс Stopwatch
для тестирования.
private static void DoTestA() {
Entity[] objects = set.GetElements();
Parallel.For(0, objects.Length, async i => {
Entity e = objects[i];
if (e == null) return;
(await e).Execute();
});
}
private static void DoTestB() {
Entity[] objects = set.GetElements();
Parallel.For(0, objects.Length, i => {
Entity e = objects[i];
if (e == null) return;
e.Execute();
});
}
Две подпрограммы идентичны, за исключением того, что один ожидает объект перед вызовом Execute() (Execute()
ничего не полезен, это просто какой-то немой код, чтобы убедиться, что процессор действительно что-то делает для каждой Entity).
После выполнения моего теста в режиме выпуска таргетинга AnyCPU, я получаю следующий вывод:
>>> 1500 repetitions >>> IN NANOSECONDS (1000ns = 0.001ms)
Method Avg. Min. Max. Jitter Total
A 1,301,465ns 1,232,200ns 2,869,000ns 1,567,534ns ! 1952.199ms
B 130,053ns 116,000ns 711,200ns 581,146ns ! 195.081ms
Как вы можете видеть, метод с ждут в нем примерно в 10 раз медленнее.
Дело в том, что, насколько мне известно, нет ничего 'ждать' GetResult
всегда верно. Означает ли это, что машина состояний выполняется, даже если ожидаемая "вещь" уже готова?
Если так, есть ли способ обойти это? Я хотел бы использовать семантику async/await, но эта служебная информация слишком высока для моего приложения...
EDIT: добавление полного кода теста после запроса:
Program.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CSharpPerfTest {
public class Entity : INotifyCompletion {
private Action continuation;
private int i;
public void OnCompleted(Action continuation) {
this.continuation = continuation;
}
public Entity GetAwaiter() {
return this;
}
public Entity GetResult() {
return this;
}
public bool IsCompleted { get { return true; } }
public void Execute() {
if (i > 0) Console.WriteLine("What");
}
}
static class Program {
static ConcurrentSet<Entity> set;
const int MAX_ELEMENTS = 50000;
// Called once before all testing begins
private static void OnceBefore() {
set = new ConcurrentSet<Entity>();
Parallel.For(0, MAX_ELEMENTS, i => {
set.Add(new Entity());
});
}
// Called twice each repetition, once before DoTestA and once before DoTestB
private static void PreTest() {
}
private static void DoTestA() {
Entity[] objects = set.GetElements();
Parallel.For(0, objects.Length, async i => {
Entity e = objects[i];
if (e == null) return;
(await e).Execute();
});
}
private static void DoTestB() {
Entity[] objects = set.GetElements();
Parallel.For(0, objects.Length, i => {
Entity e = objects[i];
if (e == null) return;
e.Execute();
});
}
private const int REPETITIONS = 1500;
private const int JIT_WARMUPS = 10;
#region Test Harness
private static double[] aTimes = new double[REPETITIONS];
private static double[] bTimes = new double[REPETITIONS];
private static void Main(string[] args) {
Stopwatch stopwatch = new Stopwatch();
OnceBefore();
for (int i = JIT_WARMUPS * -1; i < REPETITIONS; ++i) {
Console.WriteLine("Starting repetition " + i);
PreTest();
stopwatch.Restart();
DoTestA();
stopwatch.Stop();
if (i >= 0) aTimes[i] = stopwatch.Elapsed.TotalMilliseconds;
PreTest();
stopwatch.Restart();
DoTestB();
stopwatch.Stop();
if (i >= 0) bTimes[i] = stopwatch.Elapsed.TotalMilliseconds;
}
DisplayScores();
}
private static void DisplayScores() {
Console.WriteLine();
Console.WriteLine();
bool inNanos = false;
if (aTimes.Average() < 10 || bTimes.Average() < 10) {
inNanos = true;
for (int i = 0; i < aTimes.Length; ++i) aTimes[i] *= 1000000;
for (int i = 0; i < bTimes.Length; ++i) bTimes[i] *= 1000000;
}
Console.WriteLine(">>> " + REPETITIONS + " repetitions >>> " + (inNanos ? "IN NANOSECONDS (1000ns = 0.001ms)" : "IN MILLISECONDS (1000ms = 1s)"));
Console.WriteLine("Method Avg. Min. Max. Jitter Total");
Console.WriteLine(
"A "
+ (String.Format("{0:N0}", (long) aTimes.Average()) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ (String.Format("{0:N0}", (long) aTimes.Min()) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ (String.Format("{0:N0}", (long) aTimes.Max()) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ (String.Format("{0:N0}", (long) Math.Max(aTimes.Average() - aTimes.Min(), aTimes.Max() - aTimes.Average())) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ ((long) aTimes.Sum() >= 10000 && inNanos ? "! " + String.Format("{0:f3}", aTimes.Sum() / 1000000) + "ms" : (long) aTimes.Sum() + (inNanos ? "ns" : "ms"))
);
Console.WriteLine(
"B "
+ (String.Format("{0:N0}", (long) bTimes.Average()) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ (String.Format("{0:N0}", (long) bTimes.Min()) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ (String.Format("{0:N0}", (long) bTimes.Max()) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ (String.Format("{0:N0}", (long) Math.Max(bTimes.Average() - bTimes.Min(), bTimes.Max() - bTimes.Average())) + (inNanos ? "ns" : "ms")).PadRight(13, ' ')
+ ((long) bTimes.Sum() >= 10000 && inNanos ? "! " + String.Format("{0:f3}", bTimes.Sum() / 1000000) + "ms" : (long) bTimes.Sum() + (inNanos ? "ns" : "ms"))
);
Console.ReadKey();
}
#endregion
}
}
Ответы
Ответ 1
Если ваша функция имеет время отклика, что 1 мс на 50 000 вызовов считается значимым, вы не должны ждать этого кода и вместо этого запускать его синхронно.
Использование асинхронного кода имеет небольшие накладные расходы, оно должно добавить вызовы функций для конечного автомата, который управляет им внутри. Если работа, выполняемая async, также мала по сравнению с накладными расходами при запуске конечного автомата, вы должны сделать код, который вам нужен, чтобы переосмыслить, если ваш код должен быть асинхронным.
Ответ 2
Преобразован в ответ из комментариев: , по-видимому, это не чистый тестовый тест.
Если вам не требуется асинхронное продолжение, просто не используйте его. Код всегда быстрее без него. Если вам это нужно, тогда ожидайте некоторые накладные расходы. Вы должны понимать, что происходит за сценой, когда вы используете определенную функцию языка/времени выполнения.
Даже если вы удалите Parallel.For
, преобразуете lambdas в методы и предотвратите inlining, все равно будет некоторое копирование struct
и await
закрытия закрытия продолжения, для поддержки функции конечного автомата (пример сгенерированного кода).
Более справедливым эталоном будет тестирование async/await
по сравнению с альтернативной реализацией с использованием замыканий обратного вызова и Task.ContinueWith
в потоке без контекста синхронизации. Я бы не ожидал существенных различий в этом случае.
На боковой ноте вы передаете async void
lambda Action
в Parallel.For
. Вы должны знать, что элемент управления выполнением вернется к Parallel.For
, как только появится 1-й await
внутри лямбда, а затем он будет по существу вызовом "огонь и забудем" вне Parallel.For
. Я действительно не могу придумать никаких полезных сценариев для этого.