Как стать автором
Обновить

Комментарии 35

// Оффтоп
Хватит, горшочек, не вари! Шарп, родненкий, ну хорошо же жили вместе уже почти два десятка лет, всё у нас получалось, и космические корабли бороздили просторы вселенной, и сервера с клиентами любили друг друга не смотря на разделяющие их моря и океаны. Но то, что щас происходит — за гранью моего восприятия. Я не знаю, сколько часов должно быть в сутках и в рабочей неделе, чтобы успевать вникать/изучать в новые конструкции и приёмы. Особенно учитывая, что среднестатистический программист разве что курьером не дорабатывает в фирме дабы экономить бюджет и всё совсем не загнулось. Или на западе реально в каждом стартапе/фирме получается выбить деньги на гору программистов, которые могут себе позволить быть настолько специализированными, что таки способны изучать и использовать все эти бесконечные навороты?

Если хочется продвигаться в карьере и расти в з/п — надо постоянно учиться.


"Пока ты спишь, кто-то качается".

Вопрос исключительно в приоритетах. Ну и не надо работать курьером. Нужна нормальная работа с .NET — пишите, если уверены, что в текущей курьерской ситуации виновата не деструктивная лень.

Вам не надо знать детали всех «наворотов» — вам надо понимать концепции, а это в целом проще. Нюансы можно разобрать по мере необходимости.
Ну и да, развиваться надо постоянно. И не только в опыте с C#/.NET.
Не изучайте.
StackOverflow можно достичь и вполне естественным способом, например, при написании сетевого пинг-понг приложения. C# по умолчанию предлагает вызвать продолжение метода немедленно, если задача уже выполнена. Обычно это полезно, т.к. сокращает накладные расходы, но может привести и к казусам.

Да, эту проблему можно решить «костылём» в виде `Task.Yield`, а проблему с накладными расходами при переключении контекста — собственным планировщиком. Но просто обойтись без `Task.Yield`, к сожалению, не получится.
А что если попробовать Task заменить на ValueTask?
Ничего не поменяется: ValueTask ничем не отличается от Task по семантике.

Боюсь, ValueTask здесь не поможет: мы создаем цепочку вызовов, а не просто возвращаем скалярное либо др. примитивное значение.

А что по затратам памяти в пике?

.NET потребляет примерно ~300 MB в пике.

Я запустил Ваш оптимизированный через StaTaskScheduler код и он выдал ~1300мс.
Я набросал то же самое на F# ~700мс.


Код
Картинка из консоли.
Первый запуск — релизная сборка Вашей версии, второй запуск — моей.
Ссылка на .Net dll + pdb


Возможно я что-то сделал не так если разница аж в два раза между языками одной и той же платформы.


P.S. я там в for ошибся, мой код на 1 итерацию больше делает :)

Ссылка на аплоад dll битая. Вот верная

у меня Ваш F# пример выдает ~850-900 ms:


Может быть, изначально использовался неоптимизированный 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 |

csharp, fsharp


чуть добавил конфигурации для


F#
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
и
C#
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);
        }
    }
}

Спасибо за результаты. Я правда не знаю о чём это говорит.
Могу только предположить что asyncWorkflow в F# менее оверхедный чем Task в C#.

Используйте, пожалуйста, BenchmarkDotNet для бенчмарков.

мысль здравая. я и сам по возможности рекомендую библиотечку для бенчмарков.
При этом у меня не возникает чувство стыда из-за её неиспользования в данном случае.


вычислять персентили и т.п. по отношению к примеру на Go (выступающим базисом), где всего-то используется пакет time, выглядит, по-моему, неспортивно.
Особенно при выдимой невооруженным глазом разнице примерно в 2 раза в плане производительности.

Кстати посоветуйте что почитать/посмотреть чтобы хорошенько вкурить async/await с полного нуля для человека, который всю жизнь использовал для параллелизации только BackgroundWorker (ну и ещё пару раз немного Akka), а теперь хочет перейти на более современное и портабельное и понять как следует, чтобы перестать думать ивентами, начать думать тасками и научиться применять их правильно и по-полной. Заранее спасибо.

Думаю, рекомендовать книги Рихтера — вещь очевидная :)
Из блогов/публикаций настоятельно рекомендую PFX Team. Stephen Toub с командой весьма подробно разбирают подходы и принципы использования TPL и все, что с ним связано.
Серия статей по C# async от Stephen Cleary (автора библиотеки AsyncEx) также интересна.

Не знаю, Ритхера все нахваливают, но по сути он очень странно и непонятно объсняет. Тот же Гольдштейн по кишкам мне зашел в разы лучше.

НЛО прилетело и опубликовало эту надпись здесь

В первом случае мне кажется что это переписывание в стиле "Сделайте фибоначчи не рекурсивным, используя 4 аргумента". На прологе баловались, знаем, плавали :)


А вот второй вот не совсем понятен, зачем нам left и right если они на одно и то же всегда указывают?.. В чем смысл этой чехарды?

ну это можно переписать как next = f(next). по сути, я оставил этот странный кусок, чтобы максимально синтаксически приблизить к примеру на Go.

Hi, the author of two original posts is here. I speak Russian, but unfortunately, I don't type well in Russian, so:

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.
P.P.S. The funny part is: I actually spent some time to find out why missing tail / prologue Task.Yield() triggers recursion in this example. And my original plan for that post was to share stack traces to show where exactly it happens, why it happens, and why this optimization in TPL actually makes a lot of sense. But I ended up explaining only the side effect of that and cut the rest — solely because of a lack of time. And now I see why it looks like I didn’t dig deeply enough into why it happens, esp. taking into account the title. So maybe it’s a good lesson for my future posts :)

hi alexyakunin, good to see you here!
feel free to reference this post :)

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);
}
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории