Мы всего лишь воспользовались не самыми глубокими знаниями о работе тредпула и применили несколько простых инструментов из фреймворка, чтобы эффективно решить очень конкретную задачу.
Несомненно, написать на C++ код, который выполняет длительную CPU-bound работу с ограниченным уровнем параллелизма, можно очень легко и просто. Я даже соглашусь, что это может быть проще, чем на C#.
Но помещая эту подзадачу внутрь лапши http-сервера, с возможностью кучи асинхронных вызовов по пути исполнения запроса, с обилием бизнес-логики, сравнить суммарные трудозатраты на разработку и длительную поддержку всего этого в совокупности на этих двух языках уже не так просто.
Статья о C# и .Net. Она о примитивах дотнета и их особенностях. Она показывает, как те или иные синтаксические конструкции сказываются на работе приложения.
И да, она показывает это на примере какого-то настоящего сервиса. И совершенно не важно какого, и что он делает. Если бы статья была об архитектуре приложений, а не о языке и фреймворке, тут были бы другие акценты. И совершенно верно, тут было бы интересно говорить про диск.
Ответ посложнее - ожидание Lazy.Value до сих пор осталось синхронным. Вот только мы теперь синхронно ждём не всей длительной CPU-bound работы по сериализации дерева, а только создания Task (что почти мгновенно). И только затем все асинхронно ждут await task, да, не занимая потоки тредпула бессмысленными ждунами. (И ExecutionAndPublication здесь всё ещё нужен, чтобы случайно не создать два Task, оба из которых будут брать единицу параллелизма из семафора и сериализовывать одно и то же дерево.)
Думаю, что справился бы. Метод про хешкод там точно должен был бы светиться, а стектрейсы должны быть одинаковыми. К сожалению, не осталось трейса, чтобы проверить, правда ли получится.
Возможно, было бы чуть сложнее ухватиться взглядом за важные детали из-за другой визуализации.
Я должен рассказать про механику публикации статей, чтобы объяснить, почему не случилось ссылки (это не оправдание, а просто jfyi и небольшая история).
Эти статьи копируются из моего внутреннего блога. Во внутреннем блоге статей больше, чем тут. Некоторые не переносятся из-за NDA или из-за слишком дорогой адаптации для внешнего читателя.
При переносе статей мы (DevRel-ы администрируют и помогают с переносом на Habr, за что им спасибо) учитываем их топологическую сортировку, если они явно ссылаются друг на друга.
В данном случае во внутреннем блоге эта статья была выпущена раньше, чем статья про DOTNET_ThreadPool_UnfairSemaphoreSpinLimit. Из-за чего в оригинале такой ссылки не могло быть. А для переноса этой статьи потребовалось приложить больше усилий, чем для статьи про DOTNET_ThreadPool_UnfairSemaphoreSpinLimit (здесь было много внутрянки). Поэтому их порядок изменился.
Проставить ссылку после всех этих манипуляций я просто не додумался (хотя, действительно, это было бы хорошо), потому что адаптация уже готовой статьи к публикации вовне - относительно механическое мероприятие :)
Несомненно, если выбирать язык под задачу с требованием иметь фокус на производительность - выбирать C# не стоит.
Но это не значит, что на C# нельзя писать эффективные и высоконагруженные приложения - ещё как можно.
И это не значит, что в задачах, решаемых на C#, да и любом другом языке с фокусом на удобстве, безопасности и скорости процесса разработки, не могут возникать задачи оптимизации.
Например, мотивация для таких задач может быть сугубо экономическая. На определённом масштабе кластера даже из C#-приложений могут достигать сотен инстансов (а то и больше). И пара недель работы инженера над оптимизацией такого приложения на условные 10% потребления ресурсов могут сполна окупиться. При этом, рост до такого масштаба - это не повод переписывать приложение на условный C++.
применять энергии исследований лучше на специальных областях
Я всецело разделяю рационалистический подход. Но всегда остаются материи из разряда "это просто интересно (и полезно)", "это весело (и полезно)", "мне это доставляет удовольствие (и, судя по всему, не только мне)". А ещё, всегда присутствует фактор глубины знания. От знания устройства инструмента, с которым ты работаешь, результат твоей работы с этим инструментом на длительном промежутке становится точно не хуже. А, я уверен, только лучше.
Производительность LINQ сильно привязана к generic-типу коллекции, типу самой коллекции, с которой происходит работа, и к собственно самой функции вызова. Ведь LINQ это не просто перечисление, это и какая-то полезная нагрузка.
Там, например, на .Net7 .Sum() на массивах чисел работает с помощью SIMD. А на каком-то сложном IEnumerable - просто складывает влоб. А в нашем случае сложение выбрано как относительно нейтральная и бесплатная полезная нагрузка. (Будь у нас какой-нибудь супер-умный компилятор, как у C++, то он бы тоже мог заметить возможность переписать код с использованием SIMD). Кстати, это и не только обсуждалось в предыдушей статье про reciprocal throughput (и отдельного внимания там заслуживают ветки в комментариях, например вот эта).
Поэтому рассматривать производительность LINQ в сравнении с перечислением foreach и циклом for в отрыве от функции и типа коллекции просто неправильно. Такое исследование, конечно, интересно, но скатится в перечисление огромного числа случаев. Больше пользы можно извлечь из чтения патчноутов: что конкретно в LINQ в новом .Net'е улучшили.
Ну вот я и не считаю что эта статья должна быть в блоке про пул потоков.
Я объяснил свою логику: один из предлагаемых мной шагов в изучении работы тредпула, один из способов понять как он работает - рассмотреть, как выглядит снаружи его работа, с точки зрения его использования через предоставляемый нам синтаксис. И в статье явно продемонстрировано, в каком именно моменте происходит смена потоков из пула, работающих над нашими задачами, как это связано с C#-синтаксисом. По-моему, это в точности о пуле потоков. Излишнюю гранулярность и "недозагруженность" оставим за скобками.
В данном случае лично я ощущаю недогруженность.
Не считаю рациональным продолжать разговор в плоскости персонального восприятия загруженности контента. Угодить всем нельзя. А оценку "зашло" или "не зашло" оставлю на интерпретацию статистики, которую могу извлечь из Хабра. В частности, это голоса за статьи и комментарии.
И, нечего скрывать, корреляция этой статистики с обсуждаемым посылом о недозагруженности явно прослеживается.
Я нахожу несколько странным отвечать на такие вопросы, но я всё же попробую.
Что-то вы обещали рассказать про пул потоков, а в итоге рассказали как оператор await работает.
В самом начале статьи есть ссылки на все предыдущие. Среди них есть блок про ThreadPool в общем смысле. Эта статья - одна из этого блока. И я считаю важным, говоря о тредпуле, говорить в том числе о синтаксисе языка, с помощью которого с этим тредпулом работают. Всё-таки большинство инженеров на сегодняшний день сначала начинают им пользоваться, а только потом погружаются в детали. И это совершенно нормально и естественно.
так и не были упомянуты возможные взаимоблокировки или переполнения стека из-за подобного поведения и решение через RunContinuationsAsynchronously
Я предпочитаю получать и делиться информацией порционно, не перегружая контент ничем лишним. Детально и тщательно разбирая какой-то конкретный случай. Если попытаться в одну статью упихнуть всё, что только можно, получится книга. Причем, как известно, такие уже есть.
Вы перечисляете в самом деле важные и интересные вещи. Но они - одни из многих других интересных. И я действительно планировал рассмотреть в рамках этой серии статей и эти темы.
Суммируя всё выше сказанное, да, возможно, Habr не идеально подходит для формата блога взаимосвязанных статей. Или я не умею им пользоваться для успешного решения такой задачи. И я не считаю полезным в каждой статье из серии делать десятки отступлений, что это не полный гайд и не весь возможный набор информации, что это серия, и что были предыдущие статьи на тему, и будут будущие, и что да, вот эти N важных кусков информации здесь не рассмотрены. На мой взгляд достаточно упомининия блока "В предыдущих сериях" в начале. С удовольствие послушаю другую точку зрения на этот счет.
Я согласен, что зря не указал явно Intel-специфичность описываемых наблюдений.
И согласен с тем, что ARM это действительно не специальное железо, а обыденность в индустрии, как и те же видеокарты.
Спасибо, что сделали это важное замечание.
В качестве забавного наблюдения могу отметить, что мы в команде изучали возможность применения ARM'а для нашего проекта (кластер большой, интересовались в том числе и с экономической точки зрения). И оказалось, что ещё десятилетие назад в коде были сделаны неявные завязки на Intel-специфику. В итоге, в том числе новые Mac-и на ARM-ах использовать в нашей команде пока что не выйдет :)
В данной статье эти алгоритмы рассматриваются исключительно с точки зрения демонстрации эффекта от branch prediction. Алгоритмы намеренно очень похожи, чтобы выполнять "одинаковое число практически одинаковых инструкций". Исходя из этого, совершенно не важно, на что они похожи.
Иначе можно начать придираться к тому, что даже в рамках текущей complexity можно сделать ещё кучу оптимизаций.
Или можно начать погружаться в огромную и очень интересную тему сортировок :)
В данной статье рассматривались только обычные промышленные Intel'ы.
В самом конце сделано не очень явное замечание, что есть случаи, когда на специальном железе существуют специальные инструкции, сильно меняющие поведение.
Всё так. В реальном мире это почти всегда не существенно.
Task.Run
- это так, для "формальной корректности и чистоты".Вам только кажется :)
Мы всего лишь воспользовались не самыми глубокими знаниями о работе тредпула и применили несколько простых инструментов из фреймворка, чтобы эффективно решить очень конкретную задачу.
Несомненно, написать на C++ код, который выполняет длительную CPU-bound работу с ограниченным уровнем параллелизма, можно очень легко и просто. Я даже соглашусь, что это может быть проще, чем на C#.
Но помещая эту подзадачу внутрь лапши http-сервера, с возможностью кучи асинхронных вызовов по пути исполнения запроса, с обилием бизнес-логики, сравнить суммарные трудозатраты на разработку и длительную поддержку всего этого в совокупности на этих двух языках уже не так просто.
Статья о C# и .Net. Она о примитивах дотнета и их особенностях. Она показывает, как те или иные синтаксические конструкции сказываются на работе приложения.
И да, она показывает это на примере какого-то настоящего сервиса. И совершенно не важно какого, и что он делает. Если бы статья была об архитектуре приложений, а не о языке и фреймворке, тут были бы другие акценты. И совершенно верно, тут было бы интересно говорить про диск.
Замечание про
Task.Run
(илиTask.Yield()
, что я менее предпочитаю) валидное. Спасибо за дополнение.Короткий ответ - да.
Ответ посложнее - ожидание Lazy.Value до сих пор осталось синхронным. Вот только мы теперь синхронно ждём не всей длительной CPU-bound работы по сериализации дерева, а только создания Task (что почти мгновенно). И только затем все асинхронно ждут await task, да, не занимая потоки тредпула бессмысленными ждунами. (И
ExecutionAndPublication
здесь всё ещё нужен, чтобы случайно не создать два Task, оба из которых будут брать единицу параллелизма из семафора и сериализовывать одно и то же дерево.)Думаю, что справился бы. Метод про хешкод там точно должен был бы светиться, а стектрейсы должны быть одинаковыми. К сожалению, не осталось трейса, чтобы проверить, правда ли получится.
Возможно, было бы чуть сложнее ухватиться взглядом за важные детали из-за другой визуализации.
В следующий раз обязательно попробую. В этих штуках с UI обязательно найдётся какая-нибудь удобная кнопка, о которой ты не знал :)
Спасибо за внимательность! Ссылку добавил.
Я должен рассказать про механику публикации статей, чтобы объяснить, почему не случилось ссылки (это не оправдание, а просто jfyi и небольшая история).
Эти статьи копируются из моего внутреннего блога. Во внутреннем блоге статей больше, чем тут. Некоторые не переносятся из-за NDA или из-за слишком дорогой адаптации для внешнего читателя.
При переносе статей мы (DevRel-ы администрируют и помогают с переносом на Habr, за что им спасибо) учитываем их топологическую сортировку, если они явно ссылаются друг на друга.
В данном случае во внутреннем блоге эта статья была выпущена раньше, чем статья про DOTNET_ThreadPool_UnfairSemaphoreSpinLimit. Из-за чего в оригинале такой ссылки не могло быть. А для переноса этой статьи потребовалось приложить больше усилий, чем для статьи про DOTNET_ThreadPool_UnfairSemaphoreSpinLimit (здесь было много внутрянки). Поэтому их порядок изменился.
Проставить ссылку после всех этих манипуляций я просто не додумался (хотя, действительно, это было бы хорошо), потому что адаптация уже готовой статьи к публикации вовне - относительно механическое мероприятие :)
Спасибо за более точное описание различия.
Спасибо за отличное дополнение!
Несомненно, если выбирать язык под задачу с требованием иметь фокус на производительность - выбирать 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#-синтаксисом. По-моему, это в точности о пуле потоков. Излишнюю гранулярность и "недозагруженность" оставим за скобками.
Не считаю рациональным продолжать разговор в плоскости персонального восприятия загруженности контента. Угодить всем нельзя. А оценку "зашло" или "не зашло" оставлю на интерпретацию статистики, которую могу извлечь из Хабра. В частности, это голоса за статьи и комментарии.
И, нечего скрывать, корреляция этой статистики с обсуждаемым посылом о недозагруженности явно прослеживается.
Спасибо :)
Я нахожу несколько странным отвечать на такие вопросы, но я всё же попробую.
В самом начале статьи есть ссылки на все предыдущие. Среди них есть блок про ThreadPool в общем смысле. Эта статья - одна из этого блока. И я считаю важным, говоря о тредпуле, говорить в том числе о синтаксисе языка, с помощью которого с этим тредпулом работают. Всё-таки большинство инженеров на сегодняшний день сначала начинают им пользоваться, а только потом погружаются в детали. И это совершенно нормально и естественно.
Я предпочитаю получать и делиться информацией порционно, не перегружая контент ничем лишним. Детально и тщательно разбирая какой-то конкретный случай. Если попытаться в одну статью упихнуть всё, что только можно, получится книга. Причем, как известно, такие уже есть.
Вы перечисляете в самом деле важные и интересные вещи. Но они - одни из многих других интересных. И я действительно планировал рассмотреть в рамках этой серии статей и эти темы.
Суммируя всё выше сказанное, да, возможно, Habr не идеально подходит для формата блога взаимосвязанных статей. Или я не умею им пользоваться для успешного решения такой задачи. И я не считаю полезным в каждой статье из серии делать десятки отступлений, что это не полный гайд и не весь возможный набор информации, что это серия, и что были предыдущие статьи на тему, и будут будущие, и что да, вот эти N важных кусков информации здесь не рассмотрены. На мой взгляд достаточно упомининия блока "В предыдущих сериях" в начале. С удовольствие послушаю другую точку зрения на этот счет.
Я согласен, что зря не указал явно Intel-специфичность описываемых наблюдений.
И согласен с тем, что ARM это действительно не специальное железо, а обыденность в индустрии, как и те же видеокарты.
Спасибо, что сделали это важное замечание.
В качестве забавного наблюдения могу отметить, что мы в команде изучали возможность применения ARM'а для нашего проекта (кластер большой, интересовались в том числе и с экономической точки зрения). И оказалось, что ещё десятилетие назад в коде были сделаны неявные завязки на Intel-специфику. В итоге, в том числе новые Mac-и на ARM-ах использовать в нашей команде пока что не выйдет :)
Да, второй "пузырёк" ничем не отличается от сортировки "вставкой".
Впрочем, аналогия с пузырьком мне кажется более удачной. Только пузырёк не "всплывает", а "тонет" до нужного уровня.
В данной статье эти алгоритмы рассматриваются исключительно с точки зрения демонстрации эффекта от branch prediction. Алгоритмы намеренно очень похожи, чтобы выполнять "одинаковое число практически одинаковых инструкций". Исходя из этого, совершенно не важно, на что они похожи.
Иначе можно начать придираться к тому, что даже в рамках текущей complexity можно сделать ещё кучу оптимизаций.
Или можно начать погружаться в огромную и очень интересную тему сортировок :)
В данной статье рассматривались только обычные промышленные Intel'ы.
В самом конце сделано не очень явное замечание, что есть случаи, когда на специальном железе существуют специальные инструкции, сильно меняющие поведение.