Я нахожу несколько странным отвечать на такие вопросы, но я всё же попробую.
Что-то вы обещали рассказать про пул потоков, а в итоге рассказали как оператор 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
```
Ваш комментарий интересен и за 5 минут я не придумал лаконичного ответа. Как минимум, будет полезно сначала посмотреть на примеры таких ответов на stackoverflow.
Но статья всё-таки не о том, как правильно писать автоматы для стейтмашины работы с Task. Поэтому я не буду обещать, что обязательно подробно отвечу на этот вопрос.
Спасибо сразу за несколько дополнительных вариантов, как можно избежать преждевременной аллокации объекта для замыкания. И за хорошее дополнение про области видимости в контексте этой оптимизационной "задачки".
Мне пришлось отклонить некоторые провокационные комментарии к вашему сообщению, поэтому спрошу за них - это же была ирония? :)
А абстрактно в вакууме - очевидно, что ко всем оптимизациям надо подходить рационально. Взвешивать стоимость работы инженера и стоимость потребления ресурсов. В жизни бывают ситуации, когда побеждает и тот и другой вариант.
А просто знать, как работают замыкания, полезно всегда.
Конечно, отдельного рассказа стоит рассмотрение различных ситуаций, когда в методе присутствует создания нескольких Action'ов (Func'ов) с замыканиями на различные наборы аргументов (пересекающихся и\или не пересекающихся). Там ух как весело!
В моём примере ровно на неё и сделан акцент, посмотрите внимательнее на скриншот. Я обвёл красным не тип System.Action, а именно <>c__DisplayClass2_0. Этот тип - и есть аллокации того самого "j".
Да, именно так. Это легко продемонстрировать, например вот так с помощью dottrace:
В этом простом примере создается 100_000 Action'ов с замыканием. Объект замыкания - объект с одним полем int.
Известно, что такой объект "весит" 24 байта. 24 * 100_000 = 2_400_000 байт, что ~2.2 MB, как и указано в трейсе. Что подтверждает, что мы создали 100_000 объектов на хипе под эти замыкания.
Конкретные данные нельзя брать в отрыве от реальных задач. В какой-то задаче оверхед от менеджмента тасок в тредпуле будет существенным для качества решения, в какой-то нет. Ведь задач, которые можно решать с помощью тредпула, много.
Также, конкретные данные нельзя брать в отрыве от какой-то конкретной реализации (и даже в отрыве от определённой конфигурации сервера, где это исполняется). Ведь способов реализации работы с тасками много.
Данная статья не преследует цель показать "вот это решение вот этой задачи в N раз лучше, чем вот то решение". В теме тредпула достаточно тяжело делать такие однозначные и прямолинейные выводы. Ведь она достаточно обширна, до выведения условной "ThreadPool throughput" идти очень далеко и глубоко, и по пути нужно не споткнуться об ThreadPool starvation, не утонуть в высоком потреблении CPU от менеджмента тасок и потоков, и так далее. И я надеюсь, что в этом блоге удастся потрогать все эти нюансы и особенности.
Эта статья, как и весь блог, преследует цель показывать интересные особенности и детали работы .NET'а (и не только). И я надеюсь, что инженеры, которые не так хорошо знакомы с обозреваемыми темами, узнают что-то новое и научатся применять полученные знания на практике. Весь блог посвящен скорее этому, чем рекомендациям "вот это лучше вот того в столько раз".
Отличный пример использования инструмента анализа эффективности работы приложения на практике! Факт наличия проблемы замечен -> нашли источник проблемы -> исправили -> подтвердили, что всё стало лучше. Без шуток, вы молодец.
Да, тема статьи немного не об этом, но механика "ухудшения производительности приложения" через "ненужный спам объектов" одинаковая. Просто в вашем случае это было более явно (сами сделали new), чем в ситуации, описанной в статье (сахар в языке скрыл от нас new).
Я сам им не пользовался и всего лишь несколько раз наблюдал опыт его использования коллегами, поэтому не могу претендовать на объективность. Но в целях "просто поделиться опытом использования" могу рассказать, что я заметил:
10-секундные трейсы с высоконагруженных .NET приложений на Linux открывались по несколько часов (в сравнении с WPA или PerfView это на два порядка дольше).
Чтобы открыть трейсы .NET- приложений с Linux приходилось плясать с бубном, но половина символов так и осталась Unknown.
Если ты хочешь заглянуть глубже, чем в .NET вызовы на Linux, то даже учитывая предыдущий пункт этот инструмент ценен и приносит много пользы.
Вполне вероятно, мы просто неправильно его готовили.
Фразы "<таким-то> цветом выделены эвенты.." расположены в тематических подпараграфах (CPU, Exceptions, ...) одного большого параграфа "Посмотрим на какую-нибудь экзотику".
В этом корневом параграфе "Посмотрим на какую-нибудь экзотику" соответствующими цветами выделены обозреваемые коллекции эвентов (на странице PerfView со всеми типами эвентов). А в подпараграфах представлены скриншоты тех страниц PerfView, которые открываются, если провалиться (даблкликом) внутрь соответствующих коллекций эвентов, которые этим самым цветом и были выделены.
Наверное, из-за большого объема это не очень очевидно считалось.
Спасибо :)
Я нахожу несколько странным отвечать на такие вопросы, но я всё же попробую.
В самом начале статьи есть ссылки на все предыдущие. Среди них есть блок про ThreadPool в общем смысле. Эта статья - одна из этого блока. И я считаю важным, говоря о тредпуле, говорить в том числе о синтаксисе языка, с помощью которого с этим тредпулом работают. Всё-таки большинство инженеров на сегодняшний день сначала начинают им пользоваться, а только потом погружаются в детали. И это совершенно нормально и естественно.
Я предпочитаю получать и делиться информацией порционно, не перегружая контент ничем лишним. Детально и тщательно разбирая какой-то конкретный случай. Если попытаться в одну статью упихнуть всё, что только можно, получится книга. Причем, как известно, такие уже есть.
Вы перечисляете в самом деле важные и интересные вещи. Но они - одни из многих других интересных. И я действительно планировал рассмотреть в рамках этой серии статей и эти темы.
Суммируя всё выше сказанное, да, возможно, Habr не идеально подходит для формата блога взаимосвязанных статей. Или я не умею им пользоваться для успешного решения такой задачи. И я не считаю полезным в каждой статье из серии делать десятки отступлений, что это не полный гайд и не весь возможный набор информации, что это серия, и что были предыдущие статьи на тему, и будут будущие, и что да, вот эти N важных кусков информации здесь не рассмотрены. На мой взгляд достаточно упомининия блока "В предыдущих сериях" в начале. С удовольствие послушаю другую точку зрения на этот счет.
Я согласен, что зря не указал явно Intel-специфичность описываемых наблюдений.
И согласен с тем, что ARM это действительно не специальное железо, а обыденность в индустрии, как и те же видеокарты.
Спасибо, что сделали это важное замечание.
В качестве забавного наблюдения могу отметить, что мы в команде изучали возможность применения ARM'а для нашего проекта (кластер большой, интересовались в том числе и с экономической точки зрения). И оказалось, что ещё десятилетие назад в коде были сделаны неявные завязки на Intel-специфику. В итоге, в том числе новые Mac-и на ARM-ах использовать в нашей команде пока что не выйдет :)
Да, второй "пузырёк" ничем не отличается от сортировки "вставкой".
Впрочем, аналогия с пузырьком мне кажется более удачной. Только пузырёк не "всплывает", а "тонет" до нужного уровня.
В данной статье эти алгоритмы рассматриваются исключительно с точки зрения демонстрации эффекта от branch prediction. Алгоритмы намеренно очень похожи, чтобы выполнять "одинаковое число практически одинаковых инструкций". Исходя из этого, совершенно не важно, на что они похожи.
Иначе можно начать придираться к тому, что даже в рамках текущей complexity можно сделать ещё кучу оптимизаций.
Или можно начать погружаться в огромную и очень интересную тему сортировок :)
В данной статье рассматривались только обычные промышленные Intel'ы.
В самом конце сделано не очень явное замечание, что есть случаи, когда на специальном железе существуют специальные инструкции, сильно меняющие поведение.
Я старался явно и многократно делать акцент на том, что цель - продемонстрировать reciprocal throughput. Ну и показать новичкам такую особенность процессора. Жаль, что оставить фокус не цели статьи не получилось.
А цели "написать самый эффективный способ сложить 16_000 long'ов из массива на C# под .Net 6" не ставилось, хотя, выглядит как забавная задача :)
Код:
ASM:
Бенчмарк:
Спасибо, джентльмены, за интересные комментарии в этой ветке. Не хотел прерывать.
Действительно, почему бы и не добавить ассемблерный код, в который превратилась C#-реализация на SIMD:
UPD: В статью тоже добавил.
Ваш комментарий интересен и за 5 минут я не придумал лаконичного ответа. Как минимум, будет полезно сначала посмотреть на примеры таких ответов на stackoverflow.
Но статья всё-таки не о том, как правильно писать автоматы для стейтмашины работы с Task. Поэтому я не буду обещать, что обязательно подробно отвечу на этот вопрос.
Спасибо сразу за несколько дополнительных вариантов, как можно избежать преждевременной аллокации объекта для замыкания. И за хорошее дополнение про области видимости в контексте этой оптимизационной "задачки".
Ваши комментарии отлично дополняют статью.
Мне пришлось отклонить некоторые провокационные комментарии к вашему сообщению, поэтому спрошу за них - это же была ирония? :)
А абстрактно в вакууме - очевидно, что ко всем оптимизациям надо подходить рационально. Взвешивать стоимость работы инженера и стоимость потребления ресурсов. В жизни бывают ситуации, когда побеждает и тот и другой вариант.
А просто знать, как работают замыкания, полезно всегда.
А вот и нет, не всегда. Я поспешил ответить на этот вопрос чуть ниже, на ваше предыдущее сообщение.
Конечно, отдельного рассказа стоит рассмотрение различных ситуаций, когда в методе присутствует создания нескольких Action'ов (Func'ов) с замыканиями на различные наборы аргументов (пересекающихся и\или не пересекающихся). Там ух как весело!
Да, под переменную j будет ещё одна аллокация.
В моём примере ровно на неё и сделан акцент, посмотрите внимательнее на скриншот. Я обвёл красным не тип
System.Action, а именно<>c__DisplayClass2_0. Этот тип - и есть аллокации того самого "j".Да, именно так. Это легко продемонстрировать, например вот так с помощью dottrace:
В этом простом примере создается 100_000 Action'ов с замыканием. Объект замыкания - объект с одним полем int.
Известно, что такой объект "весит" 24 байта. 24 * 100_000 = 2_400_000 байт, что ~2.2 MB, как и указано в трейсе. Что подтверждает, что мы создали 100_000 объектов на хипе под эти замыкания.
Конкретные данные нельзя брать в отрыве от реальных задач. В какой-то задаче оверхед от менеджмента тасок в тредпуле будет существенным для качества решения, в какой-то нет. Ведь задач, которые можно решать с помощью тредпула, много.
Также, конкретные данные нельзя брать в отрыве от какой-то конкретной реализации (и даже в отрыве от определённой конфигурации сервера, где это исполняется). Ведь способов реализации работы с тасками много.
Данная статья не преследует цель показать "вот это решение вот этой задачи в N раз лучше, чем вот то решение". В теме тредпула достаточно тяжело делать такие однозначные и прямолинейные выводы. Ведь она достаточно обширна, до выведения условной "ThreadPool throughput" идти очень далеко и глубоко, и по пути нужно не споткнуться об ThreadPool starvation, не утонуть в высоком потреблении CPU от менеджмента тасок и потоков, и так далее. И я надеюсь, что в этом блоге удастся потрогать все эти нюансы и особенности.
Эта статья, как и весь блог, преследует цель показывать интересные особенности и детали работы .NET'а (и не только). И я надеюсь, что инженеры, которые не так хорошо знакомы с обозреваемыми темами, узнают что-то новое и научатся применять полученные знания на практике. Весь блог посвящен скорее этому, чем рекомендациям "вот это лучше вот того в столько раз".
Отличный пример использования инструмента анализа эффективности работы приложения на практике! Факт наличия проблемы замечен -> нашли источник проблемы -> исправили -> подтвердили, что всё стало лучше. Без шуток, вы молодец.
Да, тема статьи немного не об этом, но механика "ухудшения производительности приложения" через "ненужный спам объектов" одинаковая. Просто в вашем случае это было более явно (сами сделали new), чем в ситуации, описанной в статье (сахар в языке скрыл от нас new).
Да, VTune тоже интересный и достойный инструмент.
Я сам им не пользовался и всего лишь несколько раз наблюдал опыт его использования коллегами, поэтому не могу претендовать на объективность. Но в целях "просто поделиться опытом использования" могу рассказать, что я заметил:
10-секундные трейсы с высоконагруженных .NET приложений на Linux открывались по несколько часов (в сравнении с WPA или PerfView это на два порядка дольше).
Чтобы открыть трейсы .NET- приложений с Linux приходилось плясать с бубном, но половина символов так и осталась Unknown.
Если ты хочешь заглянуть глубже, чем в .NET вызовы на Linux, то даже учитывая предыдущий пункт этот инструмент ценен и приносит много пользы.
Вполне вероятно, мы просто неправильно его готовили.
Фразы "<таким-то> цветом выделены эвенты.." расположены в тематических подпараграфах (CPU, Exceptions, ...) одного большого параграфа "Посмотрим на какую-нибудь экзотику".
В этом корневом параграфе "Посмотрим на какую-нибудь экзотику" соответствующими цветами выделены обозреваемые коллекции эвентов (на странице PerfView со всеми типами эвентов). А в подпараграфах представлены скриншоты тех страниц PerfView, которые открываются, если провалиться (даблкликом) внутрь соответствующих коллекций эвентов, которые этим самым цветом и были выделены.
Наверное, из-за большого объема это не очень очевидно считалось.