Комментарии 35
// Оффтоп
Хватит, горшочек, не вари! Шарп, родненкий, ну хорошо же жили вместе уже почти два десятка лет, всё у нас получалось, и космические корабли бороздили просторы вселенной, и сервера с клиентами любили друг друга не смотря на разделяющие их моря и океаны. Но то, что щас происходит — за гранью моего восприятия. Я не знаю, сколько часов должно быть в сутках и в рабочей неделе, чтобы успевать вникать/изучать в новые конструкции и приёмы. Особенно учитывая, что среднестатистический программист разве что курьером не дорабатывает в фирме дабы экономить бюджет и всё совсем не загнулось. Или на западе реально в каждом стартапе/фирме получается выбить деньги на гору программистов, которые могут себе позволить быть настолько специализированными, что таки способны изучать и использовать все эти бесконечные навороты?
Если хочется продвигаться в карьере и расти в з/п — надо постоянно учиться.
"Пока ты спишь, кто-то качается".
Вопрос исключительно в приоритетах. Ну и не надо работать курьером. Нужна нормальная работа с .NET — пишите, если уверены, что в текущей курьерской ситуации виновата не деструктивная лень.
Ну и да, развиваться надо постоянно. И не только в опыте с C#/.NET.
Да, эту проблему можно решить «костылём» в виде `Task.Yield`, а проблему с накладными расходами при переключении контекста — собственным планировщиком. Но просто обойтись без `Task.Yield`, к сожалению, не получится.
Я запустил Ваш оптимизированный через StaTaskScheduler
код и он выдал ~1300мс.
Я набросал то же самое на F# ~700мс.
Код
Картинка из консоли.
Первый запуск — релизная сборка Вашей версии, второй запуск — моей.
Ссылка на .Net dll + pdb
Возможно я что-то сделал не так если разница аж в два раза между языками одной и той же платформы.
P.S. я там в for ошибся, мой код на 1 итерацию больше делает :)
Ссылка на аплоад dll битая. Вот верная
Может быть, изначально использовался неоптимизированный C#-пример?
Подозреваю что есть ещё что-то, т.к. я точно использовал оптимизированный пример.
Учитывая такую непостоянность результатов, нельзя сказать что они у кого-то из нас достоверные.
можно поинтересоваться характеристиками Вашей тестовой среды?
хотелось бы увидеть в таком случае результаты Go-бенчмарка как базиса тоже.
Processor=Intel Core i5-3450 CPU 3.10GHz (Ivy Bridge), ProcessorCount=4
Есть предложение лучше — прогнать с BenchmarkDotNet:
F# — у меня выдаёт 56.29 ms (я снизил кол-во итераций в коде до 100к)
C# — у меня выдаёт 355.1 ms
Попробуйте запустить тот же код в релизе (я собирал всё под 4.6.1, т.к. ParallelExtensionsExtra под netcore нет)
занятно! я попробую это дело прогнать у себя тоже.
p.s.
очевидно, что можно просто скопировать исходник StataskScheduler из ParallelExtensionsExtra прямо в проект :D
изначально я запускал пример на F# без Server GC ((
вот что у меня получилось после правки:
Method | Count | Mean | Error | StdDev |
------- |-------- |---------:|---------:|---------:|
C# | 1000000 | 592.1 ms | 97.91 ms | 15.18 ms |
F# | 1000000 | 243.0 ms | 91.02 ms | 15.04 ms |
чуть добавил конфигурации для
module Test
open BenchmarkDotNet
open BenchmarkDotNet.Running
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Configs
open BenchmarkDotNet.Jobs
open BenchmarkDotNet.Columns
let f input =
async {
let! await = input
return 1 + await
}
type DefaultConfig() as self =
inherit ManualConfig()
do
self.Add(Job.RyuJitX64.WithTargetCount(4).WithGcServer(true).WithGcForce(false))
self.Add(BaselineScaledColumn.ScaledStdDev)
[<Config(typedefof<DefaultConfig>)>]
type Worker() =
[<Benchmark>]
member public x.DoWork() =
let src = async { return -1 }
let first = f src
let mutable output = first
for i = 1 to x.Count do
let input = f output
output <- input
output
|> Async.Ignore
|> Async.RunSynchronously
[<Params(1000000)>]
member val Count = 0 with get, set
[<EntryPoint>]
let main argv =
let summary = BenchmarkRunner.Run<Worker>()
printfn "%A" summary
0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Schedulers;
namespace Test
{
[OrderProvider(SummaryOrderPolicy.FastestToSlowest)]
[Config(typeof(Config))]
public class Worker
{
private class Config : ManualConfig
{
public Config()
{
Add(Job.RyuJitX64.WithTargetCount(4).WithGcServer(true));
Add(BaselineScaledColumn.ScaledStdDev);
}
}
async Task<int> f(Task<int> input)
{
return 1 + await input; // return output
}
[Benchmark]
public void DoWork()
{
Task.Factory.StartNew(() =>
{
var tcs = new TaskCompletionSource<int>();
(var left, var right) = ((Task<int>)null, f(tcs.Task));
for (var i = 0; i < Count; i++)
{
left = f(right);
right = left;
}
tcs.SetResult(-1);
}, CancellationToken.None, TaskCreationOptions.None, new StaTaskScheduler(2)).Wait();
}
[Params(1000000)]
public int Count { get; set; }
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<Worker>();
Console.WriteLine(summary);
}
}
}
мысль здравая. я и сам по возможности рекомендую библиотечку для бенчмарков.
При этом у меня не возникает чувство стыда из-за её неиспользования в данном случае.
вычислять персентили и т.п. по отношению к примеру на Go (выступающим базисом), где всего-то используется пакет time
, выглядит, по-моему, неспортивно.
Особенно при выдимой невооруженным глазом разнице примерно в 2 раза в плане производительности.
Думаю, рекомендовать книги Рихтера — вещь очевидная :)
Из блогов/публикаций настоятельно рекомендую PFX Team. Stephen Toub с командой весьма подробно разбирают подходы и принципы использования TPL и все, что с ним связано.
Серия статей по C# async от Stephen Cleary (автора библиотеки AsyncEx) также интересна.
В первом случае мне кажется что это переписывание в стиле "Сделайте фибоначчи не рекурсивным, используя 4 аргумента". На прологе баловались, знаем, плавали :)
А вот второй вот не совсем понятен, зачем нам left и right если они на одно и то же всегда указывают?.. В чем смысл этой чехарды?
Hi, thanks for looking into this! First, let me comment on a few points you’ve mentioned:
— “The example uses a kind of anti-pattern for async usage in C#” — that’s true, and I’ve mentioned it’s an unfair benchmark for C#. I intentionally took a well-known benchmark for goroutines knowing it’s highly disadvantageous for C# to demonstrate that async/await should be comparable even in this case in terms of performance.
— This explains why I didn’t try to use other schedulers — frankly, I should, but that’s not what you normally do by default.
— This also explains why I had to use Task.Yield(). It was clear to me that you can get rid of it w/ your own scheduler, and moreover, it will also improve the speed a lot.
— I am also 90% sure that getting rid of “await Task.Yield()” is the main driver of performance improvement in your version.
— As for GC.Collect() after warmup, true, it’s really not that important, all you might expect is a bit better + more consistent timings if your working set is fully fitting into CPU caches. Unfortunately, I’ve forgot to do the same in Go test — clearly, it has to be done the same way at least.
As a side note, I never used StaTaskScheduler before — this is also why I was hesitant to invest into researching on what can be achieved with other schedulers. But thanks to you, I’ll take this into account next time, though I still lean to implementing a “perfectly slim” scheduler + probably, tasks as well. Based on your research, this should demonstrate that in this case C# should be light-years ahead, which is actually what I’d love to show: the asynchronous computation model there is way more extendable than in Go, so if it’s also better in terms of performance — even though you need some special tuning — this basically undermines the core promise Go makes.
Finally, performance isn’t the only focus point in this article. I mostly wanted to show how these languages differ in terms of their approach to asynchronous computation, and obviously, you can’t do this w/o such benchmarks.
No matter what, thanks a lot for quite useful feedback!
P.S. If you don’t mind, I’d love to reference your post if I’ll decide to publish another post on async/await vs Go here. I plan to do this closer to .NET Core 2.1 release — likely, with more benchmarks in general. Based on what’s known so far, we should expect pretty dramatic improvements in speed for C# with this release.
hi alexyakunin, good to see you here!
feel free to reference this post :)
yeah, Task.Yield
was the main performance pitfall in the original benchmarks.
btw, I'll be waiting for your next post about this topic)
Could you also test F# Hopac (which is the same async model as Go)?..
I predict it will be dramatically faster than C# and Go.
Натолкнулся я тут в будущем на эту статью из прошлого...
На всякий случай сообщаю нормальный вариант разрыва рекурсии без просадок производительности и без переусложнения кода:
static async Task<int> CountRecursivelyAsync(int count)
{
if (count <= 0)
return count;
if (!RuntimeHelpers.TryEnsureSufficientExecutionStack())
await Task.Yield();
return 1 + await CountRecursivelyAsync(count - 1);
}
Мириады запущенных задач на C#