Pull to refresh

Comments 61

Мы запускаем N параллельных задач

Let’s launch N concurrent tasks,

А уж сам автор просто образец желтой журналистики, сравнивает треды с промисами.

UFO just landed and posted this here

А что не так с переводом concurrent как параллельный?

Тем что это разные вещи.

Concurrent tasks - задачи, исполняемые одновременно (параллельно относительно друг друга)

Интересное определение но оно противоречит общепринятому. https://en.wikipedia.org/wiki/Concurrency_(computer_science) То что вы дали это на самом деле определение параллельного исполнения. А конкурентные задачи это задачи которые друг от друга не зависимы, они могут выполняться как последовательно так параллельно, это роли не играет.

Насчет "сравнения тредов с промисами" вы тоже мимо: реализации на системных потоках добавлены лишь для классического примера значительной деградации способности их плодить при росте количества задач (вплоть до отказа).

Ну и какой смысл сравнивать разные вещи?

Реализации для каждого языка на системных тредах предшествуют реализациям на виртуальных.

Не для каждого.

Тем что это разные вещи.

Может знаете какой-нибудь перевод на русский для concurrent в смысле задач?

По моему разница которая есть в терминах на английском для данного текста особо не существенна, тест вполне интересен т.к. показывает накладные расходы разных подходов когда вот такие параллельные задачи делают ничего (голый sleep). Сравнение с тредами для наглядности тоже не лишнее.

Другой вопрос как тут уже написали ниже будет ли разница в оверхеде иметь значение когда у тебя вместо sleep реальная нагрузка (типа буферов для I/O, различные состояния, итд). Тем ни менее все равно интересно знать какая разница в "минималке" у различных языков.

Одновременный.

Может знаете какой-нибудь перевод на русский для concurrent в смысле задач?

Вариант 1. Конкурентность (контекст употребления см. ниже):

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

Вариант 2. Кооперативность (контекст употребления см. ниже):

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

При кооперативной многозадачности важно не совершать длительных операций, а если и совершать — то периодически передавать управление.

Идеальным вариантом будет, если ваша «кооперативная часть» не будет работать с блокирующим I/O и мощными вычислениями, а будет использовать неблокирующее асинхронное API, а эти времязатратные вещи будут вынесены «вовне», где будут выполняться параллельно «псевдопараллельности».

В случае с asyncio и trio в Python возможно подходят оба термина (если понятие кооперативность применимо в рамках одного процесса).

Оператор await вроде бы и есть явная передача управления другим сопрограммам (с циклом событий в качестве посредника).

В любом случае, "одновременный" как вариант по умолчанию в Google Translate -- это скорее приблизительный перевод слова в общем житейском смысле.

Для сравнения, среди определений concurrent в англоязычном Викисловаре есть одно с пометкой "(computing, of code)":

(computing, of code) Designed to run independently, rather than sequentially, using various mechanisms, such as threads, event loops or time-slicing.

Соглашусь с предыдущим оратором, как минимум в .Net запустить 10К тасок != запустить 10К параллельных задач. Так-то асинхронность и на одном потоке бывает.

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

Ещё один сравниот. Берём первый же код на Расте и читаем первые строки и... num_threads это сколько? Если по числу задач, то код дурак писал. Если по числу ядер CPU, то что с чем сравниваем дальше? И почему не rayon тогда?

Берём первый же код на Расте и читаем первые строки и... num_threads это сколько

Ответ на этот вопрос можно найти в формулировке задачи.

Если по числу задач, то код дурак писал

Код писал chatgpt и кстати в полном соответствии с задачей.

то что с чем сравниваем дальше

Ответа на этот вопрос нет даже в оригинале статьи.

И почему не rayon тогда?

Точный ответ не известен но скорее всего автор либо о нем не знал либо избегал фреймворк не мейнстримных.

Подскажите пожалуйста, корректно ли в этом случае считать размер потребляемой памяти как количество потоков умноженное на размер стека для одного потока (Xss = 512 кб в java) ? Чем обусловлено различие между обычными и виртуальными потоками?

Разница примерно такая, как между аппаратными потоками и корутинами. Формально ОС может аллоцировать физическую память, только в момент когда она будет нужна, то есть page fault, и это работает для стэка, по крайней мере в линукс. Какие дополнительные факторы вводит Java VM, я не знаю, но думаю на логику ядра не должно сильно влиять.

Для короутин в .NET - точно нет, они требуют только память для state machine и переменных, а стек выделяется только при активном исполнении, при асинхронном ожидании стек не используется.
Про другие языки не уверен, но наверное эта память выделяется тоже только на физические потоки.
Но главное, автор не указал как измерял потребление памяти. XSS это в основном виртуальная память, реально используется мало. Скорее всего виртуальную память он не включал.

В Java размер памяти скорее всего будет меньше чем Xss * n_threads. Во-первых, стек спящего виртуального потока находится в куче и выделяется динамически, блоками (которые меньше Xss). Во-вторых, если у разных потоков первые (нижние) блоки стека совпадают, то они переиспользуются.

В этой статье я углублённо сравню потребление

Нет, в этой статье весьма поверхностно что-то сравнивается непонятно зачем.

И как вишенка на торте (только то, что я знаю лучше):

  1. Бенчмарк без прогрева jvm :) а старт ух как много занимает

  2. Виртуальные потоки в java еще только preview и недоступны без дополнительных флагов запуска jvm. Еще сыро

  3. С++ нет в сравнении :)

  4. Автор не пользуется формулой расчета количества потоков: операции неблокирующие => в пределах количества ядер cpu; блокирующие => создавать как можно больше потоков.

  5. Бессмысленное создание огромного количества потоков. Отсутствует нивелирование тред пулом, о реактивном подходе и говорить не приходится.

  6. Для его задач можно указать размер стека треда по дефолту в java. в hotspot там 1mb, но могу ошибаться. если сделать в два раза ниже, то и графики будут лучше :)

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

Без прогрева JVM

Сравнивается же память, а не время. Кажется, прогрев здесь не поможет, нет? Мне честно интересно, я с JVM почти не сталкиваюсь, на самом деле.

С++ нет в сравнении

А в нём есть какой-то достаточно меинстримный event loop? Всё-таки здесь по большей части сравниваются рантаймы, чем умение запустить миллион системных тредов из десяти разных языков. Опять не знаю ответа, на самом деле.

Автор не пользуется формулой расчета количества потоков

Если я всё правильно понимаю, то тут как раз таки блокирующий sleep и имеет смысл именно что запускать миллион тасок одновременно: на CPU нагрузки считай что нет. А вот память на обработку этих тасок действительно интересно поглядеть.

Отсутствует нивелирование тред пулом,

Большинство здесь приведённых рантаймов как раз таки раскидывают таски по тредам из своего пула. Не ручками же это реализовать, коли язык уже содержит довольно хорошую реализацию.

прогрев здесь не поможет, нет? Мне честно интересно, я с JVM почти не сталкиваюсь, на самом деле

Надо просто хотя бы вычесть размер памяти для jvm. Но опять же, если это нормально, то и не надо может быть.

как раз таки раскидывают таски по тредам из своего пула. Не ручками же это реализовать

Либо я не понял. Либо вы. Но в коде вы просто создаете платформенные потоки в Java при чем их число равно количеству задач

Не понял как вы смогли таким образом 100к запустить потоков. У вас возможно зависнуть должно было еще ранее. Но что точно, так миллион задач вы бы реально не смогли. Но все таки в диаграмму и по c100k и по c1000k вывели записали какие-то данные. Не могли бы вы подсказать сразу откуда ? В моем понимании должно было зависнуть у вас. На железе, которое мощнее и 200к не запустить.

А в нём есть какой-то достаточно меинстримный event loop?

Вы имеете ввиду event-loop из ui движка ? Мне кажется мы о разном... На c++ много что есть. Попробуйте реактивное программирование на нем, если вам именно фреймворки интересуют или подходы какие-то.

Если я всё правильно понимаю, то тут как раз таки блокирующий sleep и имеет смысл именно что запускать миллион тасок одновременно

На самом деле у вас не запустились таски одновременно и вы кстати не проверили результат каждого потока. Так что и не уверен, что какая-нибудь платфрома не забила и не оптимизировала так, что можно не запускать бесполезный поток или что-то с ним сделать. Попробуйте рамдомное число брать от каждого потока и складывать куда-нибудь в коллекцию по окончанию. Потом посчитать. Было бы интересно увидеть как изменятся графики

Но все таки в диаграмму и по c100k и по c1000k вывели записали какие-то данные.

Вариант с платформенными тредами как раз таки (ожидаемо) довольно быстро вылетел из соревнования и с большими числами запускался уже только Thread.startVirtualThread

На самом деле у вас не запустились таски одновременно

Они определённо не запустились in parallel — ни ядер, ни системных потоков никак не хватит. Но я уверен, что объекты тасок создавались и каждая concurrently ждала своего sleep(10) — ни одна таска не завершилась раньше десяти секунд. Кажется в этом же и суть бенчмарка — насоздавать (легковесных) тасок и посмотреть накладные расходы. А внутри них sleep чтобы весь миллион действительно висел в памяти в один момент времени.

Так что и не уверен, что какая-нибудь платфрома не забила и не оптимизировала

I/O-bound таске и не нужен отдельный поток для себя одной, а у нас здесь именно такие. При этом я плохо представляю как платформа может оптимизировать запуск миллиона тасок ждущих 10 секунд в что-то более оптимальное вроде «одна таска которая ждёт 10+ε секунд». У нас же есть сайд-эффект, что программа выполняется вполне конкретное время, его надо как-то сохранить.

Было бы интересно увидеть как изменятся графики

К сожалению, я не автор, а лишь простой мимокрокодил =)

Просто ChatGPT пока ещё не умеет плюсы, опыта не хватает :)

По факту будет аналогичный расту результат, особенно с компилятором на llvm

Уверен, автор решил не рассматривать их ещё и потому, что оч многое в сишке зависит от наддоченной настройкой компиляции. Ребята в комментах под статьями про низкие уровни мериются, у кого подобный хелоу уорлд выполнится на нс быстрее других и на 1 байт меньше аллоцируют памяти

Миллион что-то вычисляющих задач не имеет практической пользы, если только не запускается на компе со сравнимым числом ядер (а это явно не наш случай).

Миллион задач занятых I/O вполне имеет смысл, но там на каждую задачу будут выделяться буферы, как ядра (для сокетов), так и на уровне приложения (для обработки данных). Эти буферы сожрут во много раз больше памяти, и на этом фоне указанные в бенчмарках числа просто потеряются.

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

Переключение задач занимает не нулевое время, что приведет к трэшингу шедулера и печальной производительности. Поэтому для сокетов при решении таких задач(c10k и c100k problem) используются механизмы async io, epoll в линукс, и iocp в windows.

В целом да, но с "поэтому" я не соглашусь. Все эти механизмы используются не потому, что реальные треды OS долго переключать, а потому, что на реальные треды выделяется стек совершенно другого размера (около 1 MB, если не путаю), поэтому миллион реальных тредов OS потребует 1 TB памяти только на стек. Вот поэтому для таких задач используют исключительно "лёгкие" треды и мапинг M лёгких тредов на N (обычно N == количеству ядер) тредов OS. Т.е. epoll (а скоро его заменят на iouring) сотоварищи нередко используется совместно с тредами OS, а не вместо них.

Epoll обычно используется с количеством потоков, которые могут плюс минус работать параллельно физически. Стэк не аллоцируется весь физически при старте потока, как я писал выше. https://unix.stackexchange.com/questions/127602/default-stack-size-for-pthreads

И микросекунда в лучшем случаи на context switch это совсем не мало.

UFO just landed and posted this here

Согласен с Вами. Добавлю, что на компе с миллионом ядер параллелизм обеспечивается не тредами, так как не бывает общей памяти на миллион ядер.

А так-то домашнему компу начинает плохеть банально при постоянном LA > Cores.

Ну и async/await вообще не является средством многозадачности.

Мне кажется, тест не объективный. Нужно в задачах хоть что-то, хотя бы цикл на увеличение общего счетчика N раз. Таким образом можно отследить, не хитрит ли компилятор/транслятор отбрасывая ничего не значащее sleep(). Да и скорость обработки переключений и запуска можно измерить.

К слову, на C# добавление в задачу простого цикла for на увеличение общего счетчика увеличивает потребление памяти в два раза, примерно до 850 Мб.

Автор тут сравнивает красное с мягким (конкурентное и параллельное выполнение). Async/await в том же самом Python выполняется на 1-м (Карл!!!) ядре. Не надо так!

Из статьи я понял только то, что, если человек не понимает что он делает, ChatGPT ему не поможет)

>Между режимами Debug и Release я особой разницы не заметил.
На самом деле оч сильно влияет
Basically debug deployment will be slower since the JIT compiler optimizations are disabled.

Задача спать 10 секунд примерно одинаковая для обоих режимов. Если бы там числа Фибоначчи считали время от времени, то можно было бы за это говорить, а так..

Смысл это обсуждать, если даже условия тестов некорректны? Даже не углубляясь в задачу это можно сказать

Блин, да не должен пользователь вообще работать с Debug сборками, априори.

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

Вопрос не в языке, а в оптимизации. Нужно сравнивать не только объем используемой памяти, но и время на исполнение кода.

Если кто-то уложится в гиг при 10 минутах, против кого-то в 100 гигов при 1 минуте, то первый вариант для кого-то предпочтительнее, а для кого-то другой.

Но, в целом, я согласен что тест нерепрезентативен. Хотя бы потому, что не учитывает вариант последовательного исполнения менее поточных, но менее затратных по времени.

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

Но вопрос на самом деле интересный. В чем разница между блокирующим ожиданием и ожиданием коллбека? И там и там есть event loop, только в одном случае он реализован в ядре, в другом - на уровне приложения. Почему тогда коллбеки лучше?

Да, можно привести множество разных доводов (типа для ядра выполняется больше проверок безопасности, поэтому получается медленнее, но почему в ядро не добавить опцию "не делать проверки" и т.п.)

Дело не в том, что лучше, а что это абсолютно разные механизмы. В одном случае ОС даёт нам свою отдельную независимую нить исполнения со собственным стеком и прочими плюшками и оверхедом, а во втором просто в табличке в памяти создаётся строчка что мол в такую-то миллисекунду или позже вызови такую функцию.
И то и то имеет свои плюсы и минусы, но сравнивать их так в тупую, ка сделано в статье - это просто бессмысленно.
Я не удивлюсь, если окажется, что тот же компилятор Го или Раста вообще соптимизировали и выкинули создание корутин/тасков и заменили просто на один 10 секундный таймер.

Да нету никакой принципиальной разницы. Разве что между stackless и stackfull корутинами есть (в данном случае в ядре stackfull корутины), но и то нужно специальные примеры подбирать, чтобы разницу увидеть.

Соптимизировать да, в теории могли, но на практике это очень маловероятно

И самое главное у каждого вот такого Пети(автор оригинала) есть свой блог, куча статей в медиуме и даже книга "Как надо писать код"(особенно у индусских Петь) и весь этот "контент" неокрепшие умы впитывают как истину)

В Go, насколько я понимаю, там стек под задачу выделяется, хоть поначалу и весьма небольшой. А в Rust же async функции стека вовсе не имеют и занимают памяти ровно столько, сколько нужно под локальные переменные и аргументы. В случае с async функцией, которая только sleep вызывает, там этой памяти может быть на пару-тройку указателей (24 б) да и только.

Бенчмарк для го написан совершенно неверно. Чтобы не расходовать стек под горутины их надо "приземлять". Если будут запросы - покажу как этот код надо переделать для миллиона потоков.

Можете статью написать?

Не знаю как другим, мне было бы интересно.

Хорошо, сделаю :)
Есть какие-то конкретные нюансы, которые интересны? или технологию в-общем?

Ну, про горутины много где описано, включая го-тур.

Интересуют более сложные примеры, чем те, что есть в документациях и везде. Как одна из частностей, стоящих для рассмотрения -- вот, вариант с миллионом потоков.
Можно рассмотреть несколько разных примеров под разные задачи.

Это просто прекрасно: сравнивать последовательный await задач с waitall. GPT - программирование, не приходя в сознание, - сегодня в тестах, завтра в production!

Я запустил код который использует tokio, и он выполняется 10 секунд. Таски выполняются паралельно, а не последовательно.

Важно, как именно всё выполняется. В реализации на Tokio сначала все таймеры записываются в список ожидания, а потом цикл с await проверяет каждый таймер на срабатывание, сравнивая текущее время с тем, что записано в списке ожидания. Естественно, все таймеры окажутся сработавшими, когда сработает первый await в цикле. И общее время работы составит 10 секунд.

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

Поэтому прямое сравнение таких вариантов кода не имеет смысла. Это не означает, что при корректном сравнении Tokio и Rust не будут выигрывать у JS. Но нужно измерять примерно одно и то же, а не выполнять для сравнения на одной из систем заведомо более сложный алгоритм.

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

Зависит от того, что вы называете "одним и тем же". Если на одном языке код пишут в одной парадигме, а на другом — в другой, то смысл сравнивать другие парадигмы (==то, как никто не пишет)? Вроде стояла задача посмотреть, что из коробки разные языки (+их инфраструктура) могут предложить, а не спуститься на уровень ассемблера и написать 10 одинаковых ассемблерных ставок.

Парадигма с promise/future везде примерно одинаковая. Что помешало автору на других языках сохранить futures в массив и точно так же их потом последовательно дождаться? Или почему он в программе на Rust не воспользовался join_all?

Потому что писал не он, а ChatGPT. Считайте, что заглянули в ужасное будущее программирования.

Простите, а в каком листинге отсутствует сохранение в массив?


Или почему он в программе на Rust не воспользовался join_all?

Вроде нет такой функции а даже если бы и была — в чем ее принципиальное отличие от join() всего в цикле? Может для производительности и есть плюсы, хотя честно говоря, я не понимаю какие в данном случае — ведь все равно нужно ждать завершения всех задач. Даже если самая первая задача в списке будет длиться дольше всего и мы застрянем на ней — после того, как она завершится, мы быстро опросим другие задачи и увидим, что они уже завершились, то есть все остальные опросы будут уже без ожиданий.


В любом случае, здесь сравнивалась память, а не производительность.

Есть такая функция https://docs.rs/futures/0.3.5/futures/future/fn.join_all.html Отличие в том, в какие очереди эти задачи будут записаны, и как эти очереди будут обработаны. Соответственно, могут быть иными и объёмы необходимой памяти. join_all, например, кучу Box создаёт.

🤷🏼‍♂️ я просто исходники посмотрел

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

Можно. Но ресурсов это потребляет заведомо меньше.

С небольшой помощью ChatGPT я смог создать эту программу всего за несколько минут

Скайнет уже среди нас.

Sign up to leave a comment.