Как стать автором
Обновить
67
0
Александр @deniaa

Пользователь

Отправить сообщение

Спасибо за отличное дополнение!

Несомненно, если выбирать язык под задачу с требованием иметь фокус на производительность - выбирать C# не стоит.

Но это не значит, что на C# нельзя писать эффективные и высоконагруженные приложения - ещё как можно.

И это не значит, что в задачах, решаемых на C#, да и любом другом языке с фокусом на удобстве, безопасности и скорости процесса разработки, не могут возникать задачи оптимизации.

Например, мотивация для таких задач может быть сугубо экономическая. На определённом масштабе кластера даже из C#-приложений могут достигать сотен инстансов (а то и больше). И пара недель работы инженера над оптимизацией такого приложения на условные 10% потребления ресурсов могут сполна окупиться. При этом, рост до такого масштаба - это не повод переписывать приложение на условный C++.

 применять энергии исследований лучше на специальных областях

Я всецело разделяю рационалистический подход. Но всегда остаются материи из разряда "это просто интересно (и полезно)", "это весело (и полезно)", "мне это доставляет удовольствие (и, судя по всему, не только мне)". А ещё, всегда присутствует фактор глубины знания. От знания устройства инструмента, с которым ты работаешь, результат твоей работы с этим инструментом на длительном промежутке становится точно не хуже. А, я уверен, только лучше.

Производительность LINQ сильно привязана к generic-типу коллекции, типу самой коллекции, с которой происходит работа, и к собственно самой функции вызова. Ведь LINQ это не просто перечисление, это и какая-то полезная нагрузка.

Там, например, на .Net7 .Sum() на массивах чисел работает с помощью SIMD. А на каком-то сложном IEnumerable - просто складывает влоб. А в нашем случае сложение выбрано как относительно нейтральная и бесплатная полезная нагрузка. (Будь у нас какой-нибудь супер-умный компилятор, как у C++, то он бы тоже мог заметить возможность переписать код с использованием SIMD). Кстати, это и не только обсуждалось в предыдушей статье про reciprocal throughput (и отдельного внимания там заслуживают ветки в комментариях, например вот эта).

Поэтому рассматривать производительность LINQ в сравнении с перечислением foreach и циклом for в отрыве от функции и типа коллекции просто неправильно. Такое исследование, конечно, интересно, но скатится в перечисление огромного числа случаев. Больше пользы можно извлечь из чтения патчноутов: что конкретно в LINQ в новом .Net'е улучшили.

Рекомендую ознакомиться со всеми статьями целиком, но я приведу ссылки на абзацы именно про sealed-классы:

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#peanut-butter

https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#analyzers

Ну вот я и не считаю что эта статья должна быть в блоке про пул потоков.

Я объяснил свою логику: один из предлагаемых мной шагов в изучении работы тредпула, один из способов понять как он работает - рассмотреть, как выглядит снаружи его работа, с точки зрения его использования через предоставляемый нам синтаксис. И в статье явно продемонстрировано, в каком именно моменте происходит смена потоков из пула, работающих над нашими задачами, как это связано с C#-синтаксисом. По-моему, это в точности о пуле потоков. Излишнюю гранулярность и "недозагруженность" оставим за скобками.

В данном случае лично я ощущаю недогруженность.

Не считаю рациональным продолжать разговор в плоскости персонального восприятия загруженности контента. Угодить всем нельзя. А оценку "зашло" или "не зашло" оставлю на интерпретацию статистики, которую могу извлечь из Хабра. В частности, это голоса за статьи и комментарии.

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

Я нахожу несколько странным отвечать на такие вопросы, но я всё же попробую.

Что-то вы обещали рассказать про пул потоков, а в итоге рассказали как оператор await работает.

В самом начале статьи есть ссылки на все предыдущие. Среди них есть блок про ThreadPool в общем смысле. Эта статья - одна из этого блока. И я считаю важным, говоря о тредпуле, говорить в том числе о синтаксисе языка, с помощью которого с этим тредпулом работают. Всё-таки большинство инженеров на сегодняшний день сначала начинают им пользоваться, а только потом погружаются в детали. И это совершенно нормально и естественно.

так и не были упомянуты возможные взаимоблокировки или переполнения стека из-за подобного поведения и решение через RunContinuationsAsynchronously

Я предпочитаю получать и делиться информацией порционно, не перегружая контент ничем лишним. Детально и тщательно разбирая какой-то конкретный случай. Если попытаться в одну статью упихнуть всё, что только можно, получится книга. Причем, как известно, такие уже есть.

Вы перечисляете в самом деле важные и интересные вещи. Но они - одни из многих других интересных. И я действительно планировал рассмотреть в рамках этой серии статей и эти темы.

Суммируя всё выше сказанное, да, возможно, Habr не идеально подходит для формата блога взаимосвязанных статей. Или я не умею им пользоваться для успешного решения такой задачи. И я не считаю полезным в каждой статье из серии делать десятки отступлений, что это не полный гайд и не весь возможный набор информации, что это серия, и что были предыдущие статьи на тему, и будут будущие, и что да, вот эти N важных кусков информации здесь не рассмотрены. На мой взгляд достаточно упомининия блока "В предыдущих сериях" в начале. С удовольствие послушаю другую точку зрения на этот счет.

Я согласен, что зря не указал явно Intel-специфичность описываемых наблюдений.

И согласен с тем, что ARM это действительно не специальное железо, а обыденность в индустрии, как и те же видеокарты.

Спасибо, что сделали это важное замечание.

В качестве забавного наблюдения могу отметить, что мы в команде изучали возможность применения ARM'а для нашего проекта (кластер большой, интересовались в том числе и с экономической точки зрения). И оказалось, что ещё десятилетие назад в коде были сделаны неявные завязки на Intel-специфику. В итоге, в том числе новые Mac-и на ARM-ах использовать в нашей команде пока что не выйдет :)

Да, второй "пузырёк" ничем не отличается от сортировки "вставкой".

Впрочем, аналогия с пузырьком мне кажется более удачной. Только пузырёк не "всплывает", а "тонет" до нужного уровня.

В данной статье эти алгоритмы рассматриваются исключительно с точки зрения демонстрации эффекта от branch prediction. Алгоритмы намеренно очень похожи, чтобы выполнять "одинаковое число практически одинаковых инструкций". Исходя из этого, совершенно не важно, на что они похожи.

Иначе можно начать придираться к тому, что даже в рамках текущей complexity можно сделать ещё кучу оптимизаций.

Или можно начать погружаться в огромную и очень интересную тему сортировок :)

В данной статье рассматривались только обычные промышленные Intel'ы.

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

Я старался явно и многократно делать акцент на том, что цель - продемонстрировать reciprocal throughput. Ну и показать новичкам такую особенность процессора. Жаль, что оставить фокус не цели статьи не получилось.

А цели "написать самый эффективный способ сложить 16_000 long'ов из массива на C# под .Net 6" не ставилось, хотя, выглядит как забавная задача :)

Код:

[Benchmark]
public long SumNaive()
{
    long result = 0;
    var bound = array.Length;
    for (int i = 0; i < bound; i++)
    {
        result += array[i];
    }
    return result;
}

[Benchmark]
public long SumNaive2()
{
    long result = 0;
    for (var i = 0; i < array.Length; i++)
    {
        result += array[i];
    }
    return result;
}

ASM:

## .NET 6.0.16 (6.0.1623.17311), X64 RyuJIT AVX2
```assembly
; benchmarks.ReciprocalThroughput.SumNaive()
       sub       rsp,28
       xor       eax,eax
       mov       rdx,[rcx+8]
       mov       ecx,[rdx+8]
       xor       r8d,r8d
       test      ecx,ecx
       jle       short M00_L01
       nop       dword ptr [rax]
       nop       dword ptr [rax+rax]
M00_L00:
       mov       r9,rdx
       cmp       r8d,[r9+8]
       jae       short M00_L02
       movsxd    r10,r8d
       add       rax,[r9+r10*8+10]
       inc       r8d
       cmp       r8d,ecx
       jl        short M00_L00
M00_L01:
       add       rsp,28
       ret
M00_L02:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3
; Total bytes of code 68
```

## .NET 6.0.16 (6.0.1623.17311), X64 RyuJIT AVX2
```assembly
; benchmarks.ReciprocalThroughput.SumNaive2()
       sub       rsp,28
       xor       eax,eax
       xor       edx,edx
       mov       rcx,[rcx+8]
       cmp       dword ptr [rcx+8],0
       jle       short M00_L01
       nop       dword ptr [rax]
       nop       dword ptr [rax]
M00_L00:
       mov       r8,rcx
       cmp       edx,[r8+8]
       jae       short M00_L02
       movsxd    r9,edx
       add       rax,[r8+r9*8+10]
       inc       edx
       cmp       [rcx+8],edx
       jg        short M00_L00
M00_L01:
       add       rsp,28
       ret
M00_L02:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3
; Total bytes of code 67
```

Бенчмарк:

// * Summary *

BenchmarkDotNet=v0.13.4, OS=Windows 10 (10.0.19045.2965)
Intel Core i7-4771 CPU 3.50GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.105
  [Host]   : .NET 6.0.16 (6.0.1623.17311), X64 RyuJIT AVX2
  .Net 6.0 : .NET 6.0.16 (6.0.1623.17311), X64 RyuJIT AVX2

Job=.Net 6.0  Runtime=.NET 6.0  

|    Method |      Mean | Code Size |
|---------- |----------:|----------:|
|  SumNaive |  9.347 us |      68 B |
| SumNaive2 |  9.465 us |      67 B |

Спасибо, джентльмены, за интересные комментарии в этой ветке. Не хотел прерывать.

Действительно, почему бы и не добавить ассемблерный код, в который превратилась C#-реализация на SIMD:

M00_L03:
       vmovupd   ymm1,[rax]
       vpaddq    ymm0,ymm0,ymm1
       add       rax,20
       cmp       rax,rdx
       jne       short M00_L03

UPD: В статью тоже добавил.

Ваш комментарий интересен и за 5 минут я не придумал лаконичного ответа. Как минимум, будет полезно сначала посмотреть на примеры таких ответов на stackoverflow.

Но статья всё-таки не о том, как правильно писать автоматы для стейтмашины работы с Task. Поэтому я не буду обещать, что обязательно подробно отвечу на этот вопрос.

Спасибо сразу за несколько дополнительных вариантов, как можно избежать преждевременной аллокации объекта для замыкания. И за хорошее дополнение про области видимости в контексте этой оптимизационной "задачки".

Ваши комментарии отлично дополняют статью.

Мне пришлось отклонить некоторые провокационные комментарии к вашему сообщению, поэтому спрошу за них - это же была ирония? :)

А абстрактно в вакууме - очевидно, что ко всем оптимизациям надо подходить рационально. Взвешивать стоимость работы инженера и стоимость потребления ресурсов. В жизни бывают ситуации, когда побеждает и тот и другой вариант.

А просто знать, как работают замыкания, полезно всегда.

А если захватывается 10 переменных, то аллокаций, получается, будет по числу переменных

А вот и нет, не всегда. Я поспешил ответить на этот вопрос чуть ниже, на ваше предыдущее сообщение.

Конечно, отдельного рассказа стоит рассмотрение различных ситуаций, когда в методе присутствует создания нескольких Action'ов (Func'ов) с замыканиями на различные наборы аргументов (пересекающихся и\или не пересекающихся). Там ух как весело!

Да, под переменную j будет ещё одна аллокация.

В моём примере ровно на неё и сделан акцент, посмотрите внимательнее на скриншот. Я обвёл красным не тип System.Action, а именно <>c__DisplayClass2_0. Этот тип - и есть аллокации того самого "j".

1

Информация

В рейтинге
Не участвует
Работает в
Зарегистрирован
Активность