Многие интересовались производительностью расширения With.
lair провёл бенчмарки и получил разницу примерно в 5 раз по сравнению с инициализационными блоками в худшую сторону. Однако, как справедливо заметил SENya1, lair воспользовался неоптимизированной перегрузкой метода, вызывающей боксинг и аллокацию массивов,
public static T With<T>(this T o, params object[] pattern) => o;
вместо
public static T With<T>(this T o) => o;
public static T With<T, A>(this T o, A a) => o;
public static T With<T, A, B>(this T o, A a, B b) => o;
public static T With<T, A, B, C>(this T o, A a, B b, C c) => o;
/* ... */
У SENya1 на оптимизированном методе получилась разница уже порядка двух раз в пользу инициализационных блоков.
Как говорится, доверяй, но проверяй. Мне стало интересно, каковы будут результаты на моей конфигурации и повлияет ли на производительность аттрибут [MethodImpl(MethodImplOptions.AggressiveInlining)].
За основу я взял слегка модифицированный код lair 'а и проверил его на .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
.NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
Код
using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Columns;
using BenchmarkDotNet.Attributes.Exporters;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;
namespace Benchmarking
{
internal class Program
{
static void Main(string[] args)
{
try
{
BenchmarkRunner.Run<WithAndWithout>();
Console.WriteLine("Done");
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey(true);
}
}
}
static class LE
{
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T To<T>(this T o, out T x) => x = o;
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T With<T, A, B>(this T o, A a, B b) => o;
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class WithAndWithout
{
private readonly string _name = Guid.NewGuid().ToString();
private readonly int _age = 42;
[Benchmark] public Person With() => new Person().To(out var p).With
(
p.Name = _name,
p.Age = _age
);
[Benchmark] public Person Without() => new Person
{
Name = _name,
Age = _age
};
}
}
Результаты на моей конфигурации показали, что код с использованием оптимизированного With практически неотличим по производительности от инициализационных блоков! Причём, атрибут AggressiveInlining не оказывает видимого влияния на результаты.
Бенчмаки запускал по много раз, заметил, что результаты могут плавать, особенно, если во время тестов загружать процессор, например, открывать браузер или запускать другие ресурсоёмкие задачи (возможно, этим объясняется большое отклонение, полученное SENya1).
Конечно, было бы интересно сравнить результаты и на других конфигурациях… На текущий момент, полученные мной, результаты демонстрируют, что оптимизированной версией расширения можно пользоваться безо всяких опасений насчёт снижения производительности.
Код, что на гитхабе, написан обдуманно и нет там копипасты с WTF-эффектами. Я перепроверил критикуемые моменты и пришёл к выводу, что ничего ошибочного в этом коде нет. Да, его можно переписать чуть иначе, но в данном контексте он функционирует полностью исправно в соответствии с моими ожиданиями и интуицией, поэтому реальной необходимости в его переписывании сейчас у меня нет.
При ответах же на комментарии, которых довольно много, я не всегда компилирую примеры, поэтому могут возникнуть огрехи, но суть примеров всё же стараюсь сохранять.
Конечно, здорово, что люди критикуют, ведь сам я много не знаю и могу упустить важное, но на некоторые вопросы могут сосуществовать разные точки зрения. И тут нет правильных и неправильных, просто есть варианты, — хотите, применяйте WhenAll, хотите, ForEach, что вам ближе.
var person = new Person
{
Name = "Abc",
Age = 28,
City = new City { Name = "Minsk" }
}
Одна из базовых фишек With-метода — сохранение структуры кода, если new Person заменить на CreatePerson(). Конечно, на простых примерах это преимущество не столь очевидно, но при возрастании сложности разница становится заметна, особенно с учётом проверок на null (?.).
Но всё же наиболее интересные бонусы — вызов методов и возможность одновременной инициализации и деконструкции объектов.
Я и говорю: ваш метод сам по себе не «безопасен», он рассчитывает, что вызываемые им методы «безопасны».
Согласен. Но это нормальная ситация в программировании иногда расчитывать на то, что метод безопасен, если так задумано изначально. Ведь никто же, как правило, не оборачивает ToString() в try-catch, хотя исключение вполне может произойти даже при таком безобидном вызове.
Нет, достаточно сделать так, чтобы ForEach собирал результаты выполнения.
В своём естественном применении ForEach ничего не собирает — это просто альтернатива классическому циклу foreach с некоторыми вариациями и бонусами. Конечно, можно исхитриться и что-то им собрать, но тогда уж лучше использовать другие методы-расширения из Linq.
Я не могу придумать сценария, где это нужно генерично (т.е., на уровне метода-расширения).
Вопрос здесь не в самом методе-расширении ForEach, поскольку он имеет нейтральную реализацию относительно тасков, а в способах его применения.
А пример может быть такой — менеджер закачек. Открываем приложение и в нашем методе-цикле возобновляем незавершённые загрузки. Если в какой-то происходит исключение, то не нужно ожидать, пока остальные завершатся, можно сразу обработать ситуацию.
.ForEach(async d => await d.Close() && Documents.Remove(d))
уже прекрасно работает.
Если вы имеете в виду, что Add возвращает не bool, то да, здесь я поторопился, но суть примера это не сильно меняет, код больше для того, чтобы проиллюстрировать некоторую общность обоих случаев, а понадобится ли подобное расширение в дальнейшем или нет, уже другой вопрос.
P.S. Я не уверен, но, возможно, если отметить методы аттрибутом [MethodImpl(MethodImplOptions.AggressiveInlining)], то результаты могут ещё немного улучшиться…
Оно «безопасно» тогда и только тогда, когда код внутри Load не бросает эксепшн. И вы об этом, опять-таки, никогда не узнаете, потому что никакого «сразу падает» или «сразу зависает» не будет.
Во-первых, в конкретном случае архитектурно задумано, что обработкой исключений занимаются сами документы, а в задачи главной вью-модели входят лишь вызовы интерфейсных методов.
Во-вторых, если всё же нужно обработать исключение, то тут как раз-таки обязательно нужны async...await, чтобы оно не прошло незамеченным мимо
На самом деле как раз «сразу падает» и будет если параметр ForEach — Action, а не Func
Однако отмечу, что в обоих случаях .ForEach(async d => await Load())
.ForEach(d => Load())
у меня исключение проигнорировалось.
Если же необходимо обрабатывать сразу агрегированную группу исключений, то стоит использовать WhenAll, в то время как ForEach позволяет сосредоточиться на обработке каждого исключения индивидуально. Где это может иметь значение? Например, у нас ряд долговыполняющихся тасков, WhenAll выбросит исключение, лишь когда все они завершатся, что может занять продолжительное время, ForEach же будет выбрасывать исключения по мере выполнения каждого таска отдельно, не дожидаясь остальных.
Ересь. У вас другой код решает другую проблему и решает её иначе — нет между ними никакой общности, кроме вашего желания копипастить.
У меня отличное мнение от вашего. Сейчас мне не важен результат выполнения метода Load, но в обозримой перспективе он вполне может пригодиться, как показано в примере ниже. Поэтому некоторую общность я наблюдаю и стараюсь сохранить.
.ForEach(async d => await d.Close() && Documents.Remove(d))
.ForEach(async d => await d.Load())
.ForEach(async d => await d.Load() && RecentDocuments.Add(d))
… и только что вы писали «я лучше сразу обнаружу проблему и исправлю». Вы уж определитесь.
Писал я про то, что предпочитаю код, который сразу падает или зависает, если что-то перемудрили, а не пытается обработать или засупрессить все-все-все вероятные и маловероятные замысловатые ошибки, которые потенциально могли бы произойти, если бы.... умный человек разработал очень умный метод, работающий в зависимости от фазы луны и положения солнца.
Проще говоря, критикуемый .ForEach(async d => await Load())
вполне работоспособное и безопасное решение, содержащее не больше подводных камней, чем сама реализация тасков. Если для кого-то такое решение выглядит неочевидным, то можно применить любое другое на свой вкус.
Да, в конкретном случае ключевые слова async...await опциональны, но для общности с другим кодом и лучшей читаемости допустим и такой вариант.
Чем больше информации узнаю из дискуссии про таски, тем больше поражаюсь их ненадёжности — никаких гарантий! )
Уже думаю, может, лучше вернуться к старому доброму ручному созданию потоков с примитивами синхронизации… или, если код уже работает как надо, всё же решать проблемы с тасками по мере их поступления, а не заботиться о гиптетических случаях со сменами контекста или внезапными Thread.Sleep…
Хотя, проверил, и с WhenAll интерфейс тоже блокируется при Thread.Sleep, просто я не был уверен в имплементации этого метода, думал, что он обернёт всё в новый таск и Thread.Sleep не будет заметен.
Если кто-то по ошибке добавит мне в асинхронный метод Task Load() блокирующий вызов Thread.Sleep(2000) или вызов с долгими вычислениями, например, то статус таска мне мало о чём скажет.
При вашем подходе, например, даже с WhenAll, программа у меня запустится, но поскольку UI не заблокируется, мне будет казаться, что это документы у меня так долго загружаются, может, оно так и нужно, связь, например, плохая с удалёным сервером при скачивании файла.
При моём подходе сразу начнёт тормозить UI, что явно сигнализирует о каких-то проблемах в асинхронных методах.
Резюмируя всё вышесказанное, я действительно заблуждался в том, что await гарантирует запуск даже холодного таска и что его нельзя убрать в критикуемом случае с Load. В остальных моментах моё интуитивное понимание тасков позволило мне написать вполне рабочий код, пусть не самый оптимальный, но и не такой уж медленный.
А вы мне предлагаете как ни в чём ни бывало игнорировать зависший или по ошибке медленно работающий таск, вместо того, чтобы явно обнаружить проблему при тестировании программы.
Вы делайте, как хотите, а я лучше сразу обнаружу проблему и исправлю.
Это не смешная шутка, даже я со своим далеко не самым глубоким знанием тасков прекрасно понимаю разницу. Если метод асинхронный, то подобные блокирующие вызовы в нём нужно оборачивать в таски, иначе толку от асинхронности метода никакой.
Можете сами перепроверить, если мои результаты не внушают доверия. Для этого скомпилируйте и запустите приложение, создайте пять-десять документов, закройте через команду «Выход», а затем переоткройте и понаблюдайте за прогресс-барами, которые отображают процесс загрузки на вкладке каждого документа.
Понял вас. На самом деле, идея расширения более простая. Это, во-первых, замена обычно цикла foreach для коллекций на метод, как у List'а, а, во-вторых, возможность вернуть коллекцию дальше в цепочку вызовов.
Конкретно таски она затрагивает лишь косвенно. С таким же успехом я мог бы использовать стандартный метод класса List и писать в нём async...await.
Я учёл ваши замечания насчёт Load и переписал через WhenAll. Теперь всё работает параллельно и асинхронно (при добавлении рандомных задержек в метод, прогресс-бары скрываются в разные моменты времени для каждого документа). Одобряете такой код?
habr.com/post/354278
Github
github.com/Makeloft-Studio/Ace/tree/master/Ace.Base/Sugar
github.com/Makeloft-Studio/Ace/tree/master/Ace.Tests/Ace.Base.Sandbox/Sugar
Bitbucket
bitbucket.org/Makeloft/ace/src/master/Ace.Base/Sugar
bitbucket.org/Makeloft/ace/src/master/Ace.Tests/Ace.Base.Sandbox/Sugar
lair провёл бенчмарки и получил разницу примерно в 5 раз по сравнению с инициализационными блоками в худшую сторону. Однако, как справедливо заметил SENya1, lair воспользовался неоптимизированной перегрузкой метода, вызывающей боксинг и аллокацию массивов,
вместо
У SENya1 на оптимизированном методе получилась разница уже порядка двух раз в пользу инициализационных блоков.
Как говорится, доверяй, но проверяй. Мне стало интересно, каковы будут результаты на моей конфигурации и повлияет ли на производительность аттрибут
[MethodImpl(MethodImplOptions.AggressiveInlining)].За основу я взял слегка модифицированный код lair 'а и проверил его на
.NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0.NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-3517U CPU 1.90GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
Frequency=2338444 Hz, Resolution=427.6348 ns, Timer=TSC
[Host] : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
DefaultJob : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 16.77 ns | 0.5297 ns | 0.4955 ns |
Without | 16.44 ns | 0.6119 ns | 0.7284 ns |
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 17.40 ns | 0.6391 ns | 0.7849 ns |
Without | 16.52 ns | 0.5868 ns | 0.5489 ns |
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-3517U CPU 1.90GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
Frequency=2338444 Hz, Resolution=427.6348 ns, Timer=TSC
.NET Core SDK=2.0.3
[Host] : .NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
DefaultJob : .NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 20.06 ns | 0.6929 ns | 0.6142 ns |
Without | 20.71 ns | 0.5962 ns | 0.4979 ns |
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 20.42 ns | 0.5245 ns | 0.4379 ns |
Without | 20.65 ns | 0.6069 ns | 0.5068 ns |
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-3517U CPU 1.90GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
Frequency=2338444 Hz, Resolution=427.6348 ns, Timer=TSC
[Host] : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
DefaultJob : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 16.52 ns | 0.5420 ns | 0.5070 ns |
Without | 15.89 ns | 0.4229 ns | 0.3955 ns |
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 21.09 ns | 0.6985 ns | 0.7173 ns |
Without | 18.68 ns | 0.4569 ns | 0.4050 ns |
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-3517U CPU 1.90GHz (Ivy Bridge), 1 CPU, 4 logical and 2 physical cores
Frequency=2338444 Hz, Resolution=427.6348 ns, Timer=TSC
.NET Core SDK=2.0.3
[Host] : .NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
DefaultJob : .NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 21.49 ns | 1.6874 ns | 2.8193 ns |
Without | 20.51 ns | 0.7460 ns | 0.7982 ns |
Method | Mean | Error | StdDev |
-------- |---------:|----------:|----------:|
With | 20.97 ns | 0.7592 ns | 0.7457 ns |
Without | 21.14 ns | 0.7364 ns | 0.7562 ns |
Результаты на моей конфигурации показали, что код с использованием оптимизированного With практически неотличим по производительности от инициализационных блоков! Причём, атрибут
AggressiveInliningне оказывает видимого влияния на результаты.Бенчмаки запускал по много раз, заметил, что результаты могут плавать, особенно, если во время тестов загружать процессор, например, открывать браузер или запускать другие ресурсоёмкие задачи (возможно, этим объясняется большое отклонение, полученное SENya1).
Конечно, было бы интересно сравнить результаты и на других конфигурациях… На текущий момент, полученные мной, результаты демонстрируют, что оптимизированной версией расширения можно пользоваться безо всяких опасений насчёт снижения производительности.
При ответах же на комментарии, которых довольно много, я не всегда компилирую примеры, поэтому могут возникнуть огрехи, но суть примеров всё же стараюсь сохранять.
Конечно, здорово, что люди критикуют, ведь сам я много не знаю и могу упустить важное, но на некоторые вопросы могут сосуществовать разные точки зрения. И тут нет правильных и неправильных, просто есть варианты, — хотите, применяйте WhenAll, хотите, ForEach, что вам ближе.
Одна из базовых фишек With-метода — сохранение структуры кода, если
new Personзаменить наCreatePerson(). Конечно, на простых примерах это преимущество не столь очевидно, но при возрастании сложности разница становится заметна, особенно с учётом проверок на null (?.).Но всё же наиболее интересные бонусы — вызов методов и возможность одновременной инициализации и деконструкции объектов.
Согласен. Но это нормальная ситация в программировании иногда расчитывать на то, что метод безопасен, если так задумано изначально. Ведь никто же, как правило, не оборачивает
ToString()вtry-catch, хотя исключение вполне может произойти даже при таком безобидном вызове.В своём естественном применении ForEach ничего не собирает — это просто альтернатива классическому циклу foreach с некоторыми вариациями и бонусами. Конечно, можно исхитриться и что-то им собрать, но тогда уж лучше использовать другие методы-расширения из Linq.
Вопрос здесь не в самом методе-расширении ForEach, поскольку он имеет нейтральную реализацию относительно тасков, а в способах его применения.
А пример может быть такой — менеджер закачек. Открываем приложение и в нашем методе-цикле возобновляем незавершённые загрузки. Если в какой-то происходит исключение, то не нужно ожидать, пока остальные завершатся, можно сразу обработать ситуацию.
Отчего же не будет?
.ForEach(async d => await d.Close() && Documents.Remove(d))
уже прекрасно работает.
Если вы имеете в виду, что Add возвращает не bool, то да, здесь я поторопился, но суть примера это не сильно меняет, код больше для того, чтобы проиллюстрировать некоторую общность обоих случаев, а понадобится ли подобное расширение в дальнейшем или нет, уже другой вопрос.
P.S. Я не уверен, но, возможно, если отметить методы аттрибутом
[MethodImpl(MethodImplOptions.AggressiveInlining)], то результаты могут ещё немного улучшиться…Во-первых, в конкретном случае архитектурно задумано, что обработкой исключений занимаются сами документы, а в задачи главной вью-модели входят лишь вызовы интерфейсных методов.
Во-вторых, если всё же нужно обработать исключение, то тут как раз-таки обязательно нужны async...await, чтобы оно не прошло незамеченным мимо
что, вероятно, подразумевалось в комметарии
Однако отмечу, что в обоих случаях
.ForEach(async d => await Load()).ForEach(d => Load())
у меня исключение проигнорировалось.
Если же необходимо обрабатывать сразу агрегированную группу исключений, то стоит использовать WhenAll, в то время как ForEach позволяет сосредоточиться на обработке каждого исключения индивидуально. Где это может иметь значение? Например, у нас ряд долговыполняющихся тасков, WhenAll выбросит исключение, лишь когда все они завершатся, что может занять продолжительное время, ForEach же будет выбрасывать исключения по мере выполнения каждого таска отдельно, не дожидаясь остальных.
У меня отличное мнение от вашего. Сейчас мне не важен результат выполнения метода Load, но в обозримой перспективе он вполне может пригодиться, как показано в примере ниже. Поэтому некоторую общность я наблюдаю и стараюсь сохранить.
.ForEach(async d => await d.Close() && Documents.Remove(d))
.ForEach(async d => await d.Load())
.ForEach(async d => await d.Load() && RecentDocuments.Add(d))
Писал я про то, что предпочитаю код, который сразу падает или зависает, если что-то перемудрили, а не пытается обработать или засупрессить все-все-все вероятные и маловероятные замысловатые ошибки, которые потенциально могли бы произойти, если бы.... умный человек разработал очень умный метод, работающий в зависимости от фазы луны и положения солнца.
Проще говоря, критикуемый
.ForEach(async d => await Load())
вполне работоспособное и безопасное решение, содержащее не больше подводных камней, чем сама реализация тасков. Если для кого-то такое решение выглядит неочевидным, то можно применить любое другое на свой вкус.
Да, в конкретном случае ключевые слова async...await опциональны, но для общности с другим кодом и лучшей читаемости допустим и такой вариант.
Уже думаю, может, лучше вернуться к старому доброму ручному созданию потоков с примитивами синхронизации… или, если код уже работает как надо, всё же решать проблемы с тасками по мере их поступления, а не заботиться о гиптетических случаях со сменами контекста или внезапными Thread.Sleep…
Хотя, проверил, и с WhenAll интерфейс тоже блокируется при Thread.Sleep, просто я не был уверен в имплементации этого метода, думал, что он обернёт всё в новый таск и Thread.Sleep не будет заметен.
Task Load()блокирующий вызовThread.Sleep(2000)или вызов с долгими вычислениями, например, то статус таска мне мало о чём скажет.При вашем подходе, например, даже с
WhenAll, программа у меня запустится, но поскольку UI не заблокируется, мне будет казаться, что это документы у меня так долго загружаются, может, оно так и нужно, связь, например, плохая с удалёным сервером при скачивании файла.При моём подходе сразу начнёт тормозить UI, что явно сигнализирует о каких-то проблемах в асинхронных методах.
Резюмируя всё вышесказанное, я действительно заблуждался в том, что await гарантирует запуск даже холодного таска и что его нельзя убрать в критикуемом случае с Load. В остальных моментах моё интуитивное понимание тасков позволило мне написать вполне рабочий код, пусть не самый оптимальный, но и не такой уж медленный.
Вы делайте, как хотите, а я лучше сразу обнаружу проблему и исправлю.
Documents.ForEach1(d => d.Expose())
.ToList().ForEach(async d => await d.Load());
Documents.ForEach1(d => d.Expose())
.ToList().ForEach(d => d.Load());
Асинхронность отслеживаю визуально по состоянию прогресс-баров. Задержки добавляю случайно в классе PlainTextDocument
Можете сами перепроверить, если мои результаты не внушают доверия. Для этого скомпилируйте и запустите приложение, создайте пять-десять документов, закройте через команду «Выход», а затем переоткройте и понаблюдайте за прогресс-барами, которые отображают процесс загрузки на вкладке каждого документа.
Кстати, на практике асинхронно и параллельно у меня работают и все вариации с ForEach (c async и без, у List и как своё расширение).
foreachдля коллекций на метод, как уList'а, а, во-вторых, возможность вернуть коллекцию дальше в цепочку вызовов.Конкретно таски она затрагивает лишь косвенно. С таким же успехом я мог бы использовать стандартный метод класса
Listи писать в нёмasync...await.Я учёл ваши замечания насчёт
Loadи переписал черезWhenAll. Теперь всё работает параллельно и асинхронно (при добавлении рандомных задержек в метод, прогресс-бары скрываются в разные моменты времени для каждого документа). Одобряете такой код?