Comments 16
Более подробно с книгой можно ознакомиться на сайте издательства.
А вот книга «Конкурентность и параллелизм на платформе .NET. Паттерны эффективного проектирования» действительно двуязычная. По тексту примеры реализации в т.ч. на F# и даже отдельные параграфы по необходимости заточены на F#. В приложениях ещё 35 страниц о функциональном программировании и F#.
В комментариях к оригинальной статье это тоже отмечено. Скорее всего автор оригинала промахнулся с прочтением il-а.
using System;
using System.Collections.Generic;
using System.Linq;
public class C {
public void M() {
var lst = new List<int>() {1, 2, 3, 4};
var x = lst.Where(v => v % 2 == 0).ToList();
}
}
И декомпиляция:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class C
{
[Serializable]
[CompilerGenerated]
private sealed class <>c
{
public static readonly <>c <>9 = new <>c();
public static Func<int, bool> <>9__0_0;
internal bool <M>b__0_0(int v)
{
return v % 2 == 0;
}
}
public void M()
{
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
List<int> source = list;
List<int> list2 = Enumerable.ToList(Enumerable.Where(source, <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, bool>(<>c.<>9.<M>b__0_0))));
}
}
Как мы видим, лямбда создается лишь однажды: <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, bool>(<>c.<>9.b__0_0))
public void DoSomething()
{
// Этот метод не может вернуть задачу,
// делегируйте асинхронный код в другой метод
_ = DoSomethingAsync();
}
Серьёзно? Где окажется исключение выброшенное в задаче, которую вернёт DoSomethingAsync?
Оно будет проглочено.
Это очень вредный совет: генерация исключения это серьёзное состояние, которое стоит обработать. «Проглатывание» исключений не дает возможности узнать, что они вообще случались.
В общем и целом async void тоже не best practice, но я знаю два распространенных сценария, когда хотелось бы асинхронщины в void функции:
1. обработчики событий (в т.ч. от UI)
2. get/set с запуском фоновых задач
Ну и тут надо смотреть логику работы: если нужна фоновая обработка задач то есть прекрасный пакет AmbientTasks, который не позволит потерять исключения и отказаться от использования async void, при этом запуская задачи в фоне без ожидания завершения оных. Это безумно удобно как при обработке событий от UI, так и для использования в сеттерах view-model для свойства используемого в двунаправленном binding которое требует некой фоновой асинхронно обработки.
В противном случае, если возврат управления уже несет полезную нагрузку, надо ждать. Или падать, если прилетело исключение.
Спасибо за перевод, но местами автор конечно даёт.
Использование лямбда-функций запускает оптимизацию компилятора, которая кэширует делегат в статическое поле, избегая аллокации. Это работает только если Filter статичный. Если нет, вы можете кэшировать делегат самостоятельно
Ок, давайте проверим:
public static IEnumerable<int> GetItems(List<int> _list) => _list.Where(Filter);
public static IEnumerable<int> GetItemsFast(List<int> _list) => _list.Where(x => Filter(x));
private static bool Filter(int i) => i % 2 == 0;
Смотрим на IL:
.method public hidebysig static class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>
GetItems(
class [System.Collections]System.Collections.Generic.List`1<int32> _list
) cil managed noinlining
{
.maxstack 8
// [18 9 - 18 36]
IL_0000: ldarg.0 // _list
IL_0001: ldnull
IL_0002: ldftn bool C::Filter(int32)
IL_0008: newobj instance void class [System.Runtime]System.Func`2<int32, bool>::.ctor(object, native int)
IL_000d: call class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/> [System.Linq]System.Linq.Enumerable::Where<int32>(class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/>, class [System.Runtime]System.Func`2<!!0/*int32*/, bool>)
IL_0012: ret
} // end of method C::GetItems
.method public hidebysig static class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>
GetItemsFast(
class [System.Collections]System.Collections.Generic.List`1<int32> _list
) cil managed noinlining
{
.maxstack 8
// [24 9 - 24 44]
IL_0000: ldarg.0 // _list
IL_0001: ldsfld class [System.Runtime]System.Func`2<int32, bool> C/'<>c'::'<>9__2_0'
IL_0006: dup
IL_0007: brtrue.s IL_0020
IL_0009: pop
IL_000a: ldsfld class C/'<>c' C/'<>c'::'<>9'
IL_000f: ldftn instance bool C/'<>c'::'<GetItemsFast>b__2_0'(int32)
IL_0015: newobj instance void class [System.Runtime]System.Func`2<int32, bool>::.ctor(object, native int)
IL_001a: dup
IL_001b: stsfld class [System.Runtime]System.Func`2<int32, bool> C/'<>c'::'<>9__2_0'
IL_0020: call class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/> [System.Linq]System.Linq.Enumerable::Where<int32>(class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/>, class [System.Runtime]System.Func`2<!!0/*int32*/, bool>)
IL_0025: ret
} // end of method C::GetItemsFast
Видим описанное поведение, но в том же ассемблере после всех оптимизаций первый вариант кмк будет шустрее (хотя я не эксперт, мб кто-то подробнее разберет какой вариант лучшее):
00007FFA5CB21690 push rbp
00007FFA5CB21691 sub rsp,30h
00007FFA5CB21695 lea rbp,[rsp+30h]
00007FFA5CB2169A xor eax,eax
00007FFA5CB2169C mov qword ptr [rbp-8],rax
00007FFA5CB216A0 mov qword ptr [rbp+10h],rcx
00007FFA5CB216A4 mov rcx,7FFA5CBE3B10h
00007FFA5CB216AE call 00007FFABC637710
00007FFA5CB216B3 mov qword ptr [rbp-8],rax
00007FFA5CB216B7 mov r8,7FFA5CB1C6A0h
00007FFA5CB216C1 mov rcx,qword ptr [rbp-8]
00007FFA5CB216C5 xor edx,edx
00007FFA5CB216C7 mov r9,7FFA5C9ED070h
00007FFA5CB216D1 call 00007FFA5CB13F60
00007FFA5CB216D6 mov rcx,qword ptr [rbp+10h]
00007FFA5CB216DA mov rdx,qword ptr [rbp-8]
00007FFA5CB216DE call 00007FFA5CB20F60
00007FFA5CB216E3 nop
00007FFA5CB216E4 lea rsp,[rbp]
00007FFA5CB216E8 pop rbp
00007FFA5CB216E9 ret
00007FFA5CB22533 mov rcx,7FFA5CBBFB50h
00007FFA5CB2253D mov edx,3
00007FFA5CB22542 call 00007FFABC637B10
00007FFA5CB22547 mov rcx,296D3342C50h
00007FFA5CB22551 mov rcx,qword ptr [rcx]
00007FFA5CB22554 mov qword ptr [rbp-18h],rcx
00007FFA5CB22558 mov rcx,qword ptr [rbp+10h]
00007FFA5CB2255C mov qword ptr [rbp-20h],rcx
00007FFA5CB22560 mov rcx,qword ptr [rbp-18h]
00007FFA5CB22564 mov qword ptr [rbp-28h],rcx
00007FFA5CB22568 cmp qword ptr [rbp-18h],0
00007FFA5CB2256D jne 00007FFA5CB225F1
00007FFA5CB22573 mov rcx,7FFA5CBE3B10h
00007FFA5CB2257D call 00007FFABC637710
00007FFA5CB22582 mov qword ptr [rbp-30h],rax
00007FFA5CB22586 mov rcx,7FFA5CBBFB50h
00007FFA5CB22590 mov edx,3
00007FFA5CB22595 call 00007FFABC637B10
00007FFA5CB2259A mov rdx,296D3342C48h
00007FFA5CB225A4 mov rdx,qword ptr [rdx]
00007FFA5CB225A7 mov qword ptr [rbp-38h],rdx
00007FFA5CB225AB mov rdx,qword ptr [rbp-38h]
00007FFA5CB225AF mov r8,7FFA5CB22138h
00007FFA5CB225B9 mov rcx,qword ptr [rbp-30h]
00007FFA5CB225BD call 00007FFA5CB13F48
00007FFA5CB225C2 mov rcx,7FFA5CBBFB50h
00007FFA5CB225CC mov edx,3
00007FFA5CB225D1 call 00007FFABC637B10
00007FFA5CB225D6 mov rcx,296D3342C50h
00007FFA5CB225E0 mov rdx,qword ptr [rbp-30h]
Идём дальше:
Этот код провоцирует две упаковки с аллокацией: одна для преобразования Options.Option2 в Enum, а другая для виртуального вызова HasFlag для структуры. Это делает этот код непропорционально дорогостоящим. Вместо этого вам следует пожертвовать читаемостью и использовать бинарные операторы
Смотрим в IL.
.method public hidebysig
instance bool IsOption2Enabled (
valuetype Options _option
) cil managed
{
// Method begins at RVA 0x2052
// Code size 18 (0x12)
.maxstack 8
IL_0000: ldarg.1
IL_0001: box Options
IL_0006: ldc.i4.2
IL_0007: box Options
IL_000c: call instance bool [System.Private.CoreLib]System.Enum::HasFlag(class [System.Private.CoreLib]System.Enum)
IL_0011: ret
} // end of method C::IsOption2Enabled
.method public hidebysig
instance bool IsOption2EnabledFast (
valuetype Options _option
) cil managed
{
// Method begins at RVA 0x2065
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.1
IL_0001: ldc.i4.2
IL_0002: and
IL_0003: ldc.i4.2
IL_0004: ceq
IL_0006: ret
} // end of method C::IsOption2EnabledFast
Ой да, действительно, боксы, ужас-ужас. А что у нас в ASM?
C.IsOption2Enabled(Options)
L0000: test dl, 2
L0003: setne al
L0006: movzx eax, al
L0009: ret
C.IsOption2EnabledFast(Options)
L0000: test dl, 2
L0003: setne al
L0006: movzx eax, al
L0009: ret
Внезапно, никаких боксов.
Как и подписки CancellationToken, континуации TaskCompletionSource зачастую инлайнятся. Это хорошая оптимизация, но она может быть причиной неявных ошибок. Например, рассмотрим следующую программу
Просто в 2020 году нужно уже разучиться писать блокирующий Wait
в коде.
Ну и так далее. Для себя вынес, что ReadWriter в дотнете фигово реализован. Хотя, учитывая ошибки в материале, возможно автор и тут немного преувеличивает. А остальное, вроде, общеизвестно, хотя возможно кто-то узнал что-то новое.
В общем, могу посоветовать порекомендовать перепроверять высказывания "экспертов", чтобы после ускорения программа не начала работать в полтора раза медленнее чем до.
Потратил ещё немного времени: если рассахарить ещё немного лямбды (и руками прописать то, что генерирует компилятор), то получится вот такое
Видно, что во втором случае ассемблера на четверть больше, виднеются ошметки MulticastDelegate и прочая непотребщина.
В общем, то что такая замена дает профит в перфомансе считаю можно считать опровергнутым. Метод группы и чище, и производительнее.
Ой да, действительно, боксы, ужас-ужас. А что у нас в ASM? Внезапно, никаких боксов.
Так ведь вы в .NET Core посмотрели. Автор и пишет, что начиная с .NET Core 2.1 это уже не актуально.
Хотя код семантически корректен, использование ключевого слова async здесь не требуется и может привести к значительным накладным расходам в высоконагруженной среде. Старайтесь избегать его, когда это возможно
О каких накладных расходах тут речь?
Лучшие практики повышения производительности в C#