Почему ждать async так медленно?
Наконец-то я получил VS2012 и получил простую демоверсию и работал над проверкой потенциального повышения производительности async и ожидаю, но, к моему сожалению, он медленнее! Возможно, я делаю что-то неправильно, но, может быть, вы можете мне помочь. (Я также добавил простое решение Threaded и работает быстрее, чем ожидалось)
В моем коде используется класс для суммирования массива на основе количества ядер в вашей системе (-1) у Mine было 4 ядра, поэтому я видел около 2х раз (2,5 потока) для потоковой передачи, но 2x медленный вниз для того же самого, но с async/await.
Код: (Обратите внимание, что вам нужно добавить ссылку на System.Management
, чтобы заставить детектор ядра работать)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Management;
using System.Diagnostics;
namespace AsyncSum
{
class Program
{
static string Results = "";
static void Main(string[] args)
{
Task t = Run();
t.Wait();
Console.WriteLine(Results);
Console.ReadKey();
}
static async Task Run()
{
Random random = new Random();
int[] huge = new int[1000000];
for (int i = 0; i < huge.Length; i++)
{
huge[i] = random.Next(2);
}
ArraySum summer = new ArraySum(huge);
Stopwatch sw = new Stopwatch();
sw.Restart();
long tSum = summer.Sum();
for (int i = 0; i < 100; i++)
{
tSum = summer.Sum();
}
long tticks = sw.ElapsedTicks / 100;
long aSum = await summer.SumAsync();
sw.Restart();
for (int i = 0; i < 100; i++)
{
aSum = await summer.SumAsync();
}
long aticks = sw.ElapsedTicks / 100;
long dSum = summer.SumThreaded();
sw.Restart();
for (int i = 0; i < 100; i++)
{
dSum = summer.SumThreaded();
}
long dticks = sw.ElapsedTicks / 100;
long pSum = summer.SumParallel();
sw.Restart();
for (int i = 0; i < 100; i++)
{
pSum = summer.SumParallel();
}
long pticks = sw.ElapsedTicks / 100;
Program.Results += String.Format("Regular Sum: {0} in {1} ticks\n", tSum, tticks);
Program.Results += String.Format("Async Sum: {0} in {1} ticks\n", aSum, aticks);
Program.Results += String.Format("Threaded Sum: {0} in {1} ticks\n", dSum, dticks);
Program.Results += String.Format("Parallel Sum: {0} in {1} ticks\n", pSum, pticks);
}
}
class ArraySum
{
int[] Data;
int ChunkSize = 1000;
int cores = 1;
public ArraySum(int[] data)
{
Data = data;
cores = 0;
foreach (var item in new System.Management.ManagementObjectSearcher("Select * from Win32_Processor").Get())
{
cores += int.Parse(item["NumberOfCores"].ToString());
}
cores--;
if (cores < 1) cores = 1;
ChunkSize = Data.Length / cores + 1;
}
public long Sum()
{
long sum = 0;
for (int i = 0; i < Data.Length; i++)
{
sum += Data[i];
}
return sum;
}
public async Task<long> SumAsync()
{
Task<long>[] psums = new Task<long>[cores];
for (int i = 0; i < psums.Length; i++)
{
int start = i * ChunkSize;
int end = start + ChunkSize;
psums[i] = Task.Run<long>(() =>
{
long asum = 0;
for (int a = start; a < end && a < Data.Length; a++)
{
asum += Data[a];
}
return asum;
});
}
long sum = 0;
for (int i = 0; i < psums.Length; i++)
{
sum += await psums[i];
}
return sum;
}
public long SumThreaded()
{
long sum = 0;
Thread[] threads = new Thread[cores];
long[] buckets = new long[cores];
for (int i = 0; i < cores; i++)
{
int start = i * ChunkSize;
int end = start + ChunkSize;
int bucket = i;
threads[i] = new Thread(new ThreadStart(() =>
{
long asum = 0;
for (int a = start; a < end && a < Data.Length; a++)
{
asum += Data[a];
}
buckets[bucket] = asum;
}));
threads[i].Start();
}
for (int i = 0; i < cores; i++)
{
threads[i].Join();
sum += buckets[i];
}
return sum;
}
public long SumParallel()
{
long sum = 0;
long[] buckets = new long[cores];
ParallelLoopResult lr = Parallel.For(0, cores, new Action<int>((i) =>
{
int start = i * ChunkSize;
int end = start + ChunkSize;
int bucket = i;
long asum = 0;
for (int a = start; a < end && a < Data.Length; a++)
{
asum += Data[a];
}
buckets[bucket] = asum;
}));
for (int i = 0; i < cores; i++)
{
sum += buckets[i];
}
return sum;
}
}
}
Любые мысли? Я делаю async/жду неправильно? Я буду рад попробовать любые предложения.
Ответы
Ответ 1
В вашем тесте есть несколько недостатков:
- Вы запускаете первый запуск, который включает время инициализации (загрузка
class Task
, JIT-компиляция и т.д.).
- Вы используете
DateTime.Now
, что слишком неточно для таймингов в миллисекундах. Вам нужно будет использовать StopWatch
С этими двумя проблемами исправлено; Я получаю следующие результаты тестов:
Regular Sum: 499946 in 00:00:00.0047378
Async Sum: 499946 in 00:00:00.0016994
Threaded Sum: 499946 in 00:00:00.0026898
Теперь Async выходит как самое быстрое решение, занимая менее 2 мс.
Это следующая проблема: синхронизация чего-то столь же быстрого, как 2 мс, крайне ненадежна; ваш поток может быть приостановлен дольше, чем если бы какой-либо другой процесс использовал CPU в фоновом режиме. Вы должны усреднить результаты за несколько тысяч эталонных тестов.
Кроме того, что происходит с вашим номером обнаружения ядра? Мой четырехъядерный процессор использует размер куска 333334, который позволяет запускать только три потока.
Ответ 2
Важно отделить "асинхронность" от "распараллеливания". await
, чтобы упростить создание асинхронного кода. Код, который работает параллельно, может (или не может) включать асинхронность, а асинхронный код может выполняться или не работать параллельно.
Ничто в await
не предназначено для ускорения параллельного кода. Целью await
является упрощение написания асинхронного кода при минимизации негативных последствий для производительности. Использование await
никогда не будет быстрее, чем правильно написанный не ожидающий асинхронный код (хотя, поскольку писать правильный код с помощью await
проще, он иногда будет быстрее, потому что программист не способен правильно записать этот асинхронный код без подождите или не захочет использовать время для этого. Если неасинхронный код написан хорошо, он будет работать также, если не лучше, чем код await
.
У С# есть поддержка специально для распараллеливания, это просто не специально, хотя await
. Параллельная библиотека задач (TPL), а также Parallel LINQ (PLINQ) имеют несколько очень эффективных средств распараллеливания кода, которые, как правило, более эффективны, чем наивные потоковые реализации.
В вашем случае эффективная реализация с использованием PLINQ может быть примерно такой:
public static int Sum(int[] array)
{
return array.AsParallel().Sum();
}
Обратите внимание, что это позаботится об эффективном разбиении входной последовательности на куски, которые будут выполняться параллельно; он позаботится о том, чтобы определить соответствующий размер кусков и количество одновременно работающих работников, и он будет соответствующим образом агрегировать результаты этих работников в усадьбе, которые были должным образом синхронизированы для обеспечения правильного результата (в отличие от вашего примера с резьбой) и эффективного (это означает, что он не будет полностью сериализовать всю агрегацию).
Ответ 3
async
не предназначен для сверхпрочных параллельных вычислений. Вы можете выполнять базовую параллельную работу с помощью Task.Run
с Task.WhenAll
, но любая серьезная параллельная работа должна выполняться с использованием параллельной библиотеки задач (например, Parallel
). Асинхронный код на стороне клиента - это отзывчивость, а не параллельная обработка.
Общим подходом является использование Parallel
для параллельной работы, а затем его перенос в Task.Run
и использование await
на нем, чтобы поддерживать пользовательский интерфейс.
Ответ 4
При быстром просмотре ожидаются результаты: ваша сумма async использует только один поток, в то время как вы асинхронно дожидаетесь его завершения, поэтому он медленнее, чем многопоточная сумма.
Вы должны использовать async, если у вас есть что-то еще, чтобы закончить, выполняя свою работу. Таким образом, это не будет правильным испытанием для любых улучшений скорости/ответа.