Skip to content

Latest commit

 

History

History
178 lines (90 loc) · 47 KB

03-02-MemoryManagement-Allocation.md

File metadata and controls

178 lines (90 loc) · 47 KB

Выделение памяти под объект

{.big-quote} Адаптация курса в процессе

Мы только что поговорили про GC с высоты птичьего полета. Сейчас будем уходить в подробности. В первую очередь, хочется пообщаться про алгоритм выделения памяти.

Управление памятью .NET разработчика интересует со стороны двух основных процессов: первое - это выделить память, второе - освободить. Про выделение памяти мы сейчас и поговорим.

С точки зрения процессора память выглядит несколько сложнее. Если говорит про архитектуру управления памяти, я бы сказал, что она разделена на слои. Первый слой, который видим мы, слой .NET достаточно сложный, но верхнеуровневый. То есть он в любом случае опирается на что-то другое, а именно на слои управления памятью Windows, который в свою очередь запущен на каком-то процессоре. Процессор по-своему интерпретирует эту память. Таким образом, у нас фактически существует три слоя управления памятью.

Память с точки зрения процессора - это планка, физическая память, RAM. Сколько планок мы поставили, столько он и видит. Но у нас есть знания, что каждый запущенный в системе Windows процесс изолирован. И кроме себя он больше ничего не видит. Есть сам процесс, winapi и больше ничего вообще. Word не видит Excel. Excel не видит калькулятор. Все они изолированы полностью. Потому что на архитектуре Intel сделана виртуализация памяти.

Как она работает. Есть таблицы глобальных дескрипторов, которые мы не видим, есть таблицы локальных дескрипторов, которые куда-то отсылаются. И все вместе это ссылается на Page Frame.

У любого процесса есть некий диапазон памяти от нуля и до, например, 4Гб на x32 системе и до условной бесконечности на x64. Это диапазон виртуальной памяти. Процессор создает для конкретной программы некий виртуальный кусок, где есть только она. Эта область поделена на страницы: на кусочки по 4Кб. Каждый их них означает, что память в этом месте либо существует, либо ее там не существует, т.е. она физически в этом месте отсутствует полностью. Адрес есть, а памяти там нет. Это возникает в ситуациях, когда планка, например на 4Гб, вставлена в материнскую плату, а процессов у нас запущена сотня и 32x разрядная Windows. Значит, что у каждой программы 4Гб памяти, но при этом они друг от друга изолированы.

Как 4 Гб поделить на сто и получить 4 каждому? Никак. Это значит, что у каждой программы на четырехгигабайтном участке есть места, где памяти нет вообще, там только выделены кусочки адресов. Но где-то память есть и она замаплена на планку физическую. А если не замаплена - памяти нет. Если туда обратиться, что-то попытаться считать, вы получите не ноль, а AccessViolationException. Исключение, которое говорит о том, что вы пытаетесь работать с куском, который не существует, либо на который у вас нет прав. На данном уровне исключение различий не делает.

Точно также работать с Wap. (см. слайд 06:30). На изображении выделены процессы в виде столбиков. Там, где белый шум - память выделена и заполнена. А там, где пропуски - памяти нет. Выделенные области могут быть замаплены как на физическую память, так и на жесткий диск.

Если, например, программа много памяти выделила, но ей не пользуется, а соседней программе память нужна, при этом планка маленькая, то Windows неиспользуемую память отгружает на жесткий диск, а этот кусок отдает кому-то другому.

Итак, в нашем .NET приложении часть памяти используется, а часть - нет. Как это можно посмотреть? Можно запустить утилиту, например есть Procmon. Они показывают, как приложение расходует память. Там видно строчки - это как раз выделенные участки. Первый столбец - это адрес участка, второй указывает хип, третий - размер участка. И еще один столбик - это commited. Что это значит? Если программа решила, что ей нужен хип, и пытается определить какой именно ей создавать. Она резервирует память под хип, и часть ее решает закоммитить. Виртуальное адресное пространство разделено на страницы. У страниц есть три статуса. Первый - свободная страница, на которой ничего нет. Ее можно брать и использовать как нужно. Второй статус - страница зарезервирована. Это значит, что какая-то часть программы под себя эту страницу зарезервировала, но там все еще нет памяти, она там отсутствует. Но страница зарезервирована для будущих нужд. Это делается для того, чтобы в аллоцированом хипе никто другой по середине себе ничего не выделил.

Может показаться, что вы как хозяин своего приложения, как хотите, так и выделяете память. На самом деле нет. Если вы чисто в .NET - то может быть да. А вообще говоря, .NET часто вызывает Managed code, которому в свою очередь тоже нужна память. И он не будет выделять память в хипах от .NET - SOH и LOH. Он о них не имеет понятия.

Он выделяет память в своих хипах. Если это С++, то это C++ Runtime Library. Но там есть свои механизмы построения хипов. Получается, что вы не полностью контролируете виртуальный адресный диапазон, который вам выдали. Чтобы обезопасить себя, вы сразу резервируете адресное пространство и внутри него уже начинаете аллоцировать память - коммитить ее, чтобы она стала существовать, чтобы туда можно было что-то писать.

На слайде 11:50 видны свободные места и есть непрерывный участок, частично зарезервированный, частично - закоммиченный. Это место может быть в оперативной памяти, либо находится на жестком диск, потому что им давно не пользовались. Если вы обращаетесь к куску, который находится на жестком диске, то он автоматически подгружается и становится куском из оперативной памяти. Но вам это знать уже не надо.

Зная все это, разберем как работает var x = new A();

Вернемся к базовым знаниям, то, что мы обычно рассказываем на собеседованиях. Когда мы общаемся с программистами, нас спрашивают, как все работает. Мы рассказываем, что есть три поколения. Как память выделяется? Указатель смотрит на свободный участок и когда делается операция new, то мы этот адрес отдаем а указатель перемещаем на размер выделенного блока. А когда происходит GC, куча сжимается и этот указатель оказывается раньше, потому что куча сжалась. Примерно так и происходит. На слайде 13:31 у нас есть закоммиченная часть - слева до вертикальной черты. Эта память существует физически. Справа от черты - это зарезервированная за хипом часть, но ее пока не существует. Дальше мы должны вспомнить о таком понятии как allocation context. Это некое скользящее окно, внутри которого в данный момент менеджер памяти выделяет память под объекты. И уже внутри него у нас есть указатели на начало свободного места. Слева уже пошли объекты.

Когда идет операция new A(), мы запоминаем этот адрес, размещаем там объект и передвигаем адрес начала свободного места, отдавая адрес в переменную x. Наш код выглядит примерно следующим образом (см.слайд 14:39). У нас есть метод Allocate(), мы попросили у него некий amount памяти. Он проверяет, есть ли у нас будущий адрес свободного места (адрес начала места плюс этот amount). Если он превысит heap size, у нас никак не получится новую память выделить, то мы бросаем OutOfMemoryException. Иначе мы спокойно отдаем этот адрес запрашивающей стороне, обновляя весь диапазон памяти, чтобы там не было лишних данных.

Но может быть так, что allocation context у нас уже кончается. Тогда будет промах мимо этого контекста. Как действовать в этом случае? Аллокатор может действовать двумя путями. Первый - это выделить, например, по-минимуму, второй - выделить сколько-то до некоего максимума. Этот размер может быть от 1 до 8Кб. И выбирается он исходя из интенсивности выделения памяти. Если на данном потоке выделяется много памяти, то нам нет никакого смысла выделять маленький кусок. То есть allocation context расширяется исходя из текущих запросов.

Но существует еще и многопоточность. Если она присутствует, может так получится, что несколько потоков одновременно начинают запрашивать выделения памяти. Значит, оператор new у нас должен быть потоко-безопасным, а значит и медленным. Сам по себе он быстрый, но если он выделяется без конкуренции с другими потоками, то получается, что он работает неоправданно медленно. Чтобы сделать new потоко-безопасным, надо чтобы каждый поток выделял память в каком-то своем месте. Поэтому в .NET существует по одному allocation context на каждый поток.

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

Итак, много потоков у нас уже существует и каждый из них что-то выделяет. А еще у нас есть три поколения. Получается, что нулевое поколение, в котором идет выделение, начинает расти благодаря тому, что у нас растет количество потоков. Это плохо.

Обратимся к идее списка свободных участков.

Если мы говорим про классику, как везде heap устроен, то .NET - не исключение. Когда происходит sweep collection вместо сжатия кучи, у нас появляются свободные участки. Ими надо как-то управлять.

Есть два алгоритма менеджмента свободных участков. Первый - best-fit. Когда выделяется память, среди всех свободных участков выделяется тот, который имеет либо такой же размер, либо максимально приближенный по размеру к тому, который просили. Это best-fit.

Когда мы используем best-fit, нам нужно найти наилучший участок. Мы пробегаем по всем участкам, и запоминаем то, что мы прошли. Выполняется линейный поиск и наш участок может оказаться в конце. Но зато на выходе мы получаем минимальную фрагментацию - это здорово. Второй алгоритм - это first-fit. Это альтернатива. Мы идем вперед по списку и берем первый же участок, который нам подошел. Он может быть таким же по размеру, либо больше или существенно больше требуемого. Работает это быстро, но может сильно фрагментировать память. В .NET используется смесь этих подходов, организуется список свободных участков через корзины - бакеты. У каждого поколения есть список бакетов. Они группируют память по размеру от малого к большому. Бакет ссылается на односвязаный список свободных участков. Если посмотреть вниз, то от Head на свободный участок встали и дальше по односвязному списку можем идти дальше, перечисляя все свободные участки, которые относятся к этому бакету. Напомню, они организованы по размеру. И в данном бакете находятся участки с определенным диапазоном размеров. В следующем бакете будут свои диапазоны.

Бакет - это first-fit, среди бакетов выбираем первый, который подходит, уходим внутрь этой корзины и там, по одноcвязному списку мы идем по best-fit. Таким образом мы делаем очень хорошую оптимизацию.

Что такое бакеты? Эта табличка (см. слайд 22:05) нам раскрывает глаза. Тут вспомним разницу между SOH и LOH: LOH организован только по принципу sweep collection, сжатие кучи там происходит только по запросу. Само по себе сжатие кучи там не запустится никогда. В SOH идет смесь.

Посмотрим на LOH и второе поколение, которое является самым большим. Исходя из этого можно легко понять содержимое таблицы. Нулевое и первое поколение имеют количество бакетов - 1. Потому что в нулевом и первом поколении у нас нет такого, что при sweep образуется куча свободных больших участков, куда можно разместить новые контексты. Нам этот функционал бакетов там не нужен, поэтому бакет там один.

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

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

Итак, мы нашли нужный участок. В этом месте лежит эмуляция объекта. Первое поле - это поле undo. Это - освободившийся участок, там лежал объект или группа объектов. Соответственно, первое поле это aSyncBlockIndex, потом - таблица виртуальных методов, потом данные. Когда этот участок стал участком свободной памяти, то первое поле стало операцией undo. Когда мы этот участок отлинкуем и сохраним ссылку на следующий участок в операции undo, чтобы можно было отменить, если что-то пойдет не так.

Дальше таблица виртуальных методов свободного участка, чтобы GC понимал, с чем он имеет дело и size - размер свободного участка.

После этого выделения мы проставляем aSyncBlockIndex. Таблица виртуальных методов становится та, что надо и вызывается конструктор. Все. Вот так работает выделение памяти. \

Выделение памяти в SOH

Получается, что для того, чтобы выделить память сначала отрабатывает самый простой способ. Если объект влезает в allocation context, мы выделяем и отдаем. Это самый быстрый способ. Мы выходим моментально, у нас нет никаких дополнительных действий: вошли и вышли, и передвинуть указатель нам ничего не стоит.

Если не влезаем в контекст, то идет первое усложнение. Контекст необходимо увеличить. Чтобы увеличить, мы используем более продвинутый способ, он находится в JIT_NEW helper.

Мы пытаемся найти неиспользуемую часть в эфемерном сегменте. Когда у нас allocation context заканчивается, мы должны его куда-то перетащить. Это дорого. Поэтому мы сначала пытаемся его увеличить, нарастить. Если это не получилось, то надо перетащить.

Куда мы должны его переместить и какие данные у нас для этого есть?

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

Если получилось - замечательно. Если не влез, мы пытаемся подкоммитить больше зарезервированной памяти: сдвинуть вертикальную черту вправо. Место зарезервировано, но памяти там еще не существует. Нам нужно расширить кучу и мы коммитим память. Сразу же мы не делаем этого, потому что commitment - операция достаточно долгая, как и резервирование. Она может занимать сотню миллисекунд. Это происходит потому, что для ее осуществления, необходимо обратиться к Windows. А операционная система максимально не доверяет тому, что в ней находится. Он отгораживает для нас песочницу, и когда мы вызываем winapi метод, то мы не просто его вызываем, а с повышением уровня привилегий. Чтобы это безопасно это сделать, необходимо, чтобы Windows скопировал фрейм метода, который вызывается из уровня приложения в уровень ядра, чтобы случайно не подхватить чужие инфицированные данные. Потом winapi функция отрабатывает и происходит возврат. В этот момент идет обратное копирование кусочка стека и идет переход из kernel части в user space. Вся эта операция занимает много времени, поэтому GC максимально старается использовать текущее пространство. И на каждом этапе он делает GC нулевого поколения: если что-то подсжимать, возможно, allocation context обратно вырастет. Когда контекст кончился, GC пытается его раздвинуть вправо, а там уже что-то есть, значит надо поискать свободные участки. Если не получилось, делается сборка мусора, может быть свободные участки все же появятся и получится разместить среди них новые данные. Когда ничего не получилось и все забито, то приходится коммитить дальше память. А если память кончилась, то вылетает OutOfMemoryException.

Выделение памяти в LOH

В хипе больших объектов мы пытаемся найти неиспользуемую память, повторяя для каждого эфемерного сегмента LOH, потому что тут их может быть несколько, в зависимости от того, в каком режиме работает платформа .NET. Поскольку в LOH по умолчанию у нас нет сжатия кучи, то управление памятью тут упрощено. Сжатие кучи - это очень жирная операция, которая требует больших вычислительных процессов. Но в LOH эта операция проводится только по запросу, а значит программист исходя из знаний о работе алгоритмов собственной программы, решил, что в данное время он готов потратить неизвестно сколько времени на сжатие огромного пространства. А значит, можно сильно упростить алгоритмы по работе с LOH.

Для каждого эфемерного сегмента мы ищем участок памяти в списке свободных, пытаемся увеличить, если не получилось найти. Если не получилось увеличить - пытаемся подкоммитить зарезервированную часть. Следующим шагом запускаем GC, возможно несколько раз. И если совсем ничего не выходит - дергаем OutOfMemoryException. В данном подходе нет слова "сжатие", мы идем по простому пути.

Многопоточность. Есть такое понятие как баланс хипов. Например, GC запущен в серверном режиме. Это значит, что у него по несколько SOH и LOH и эти пары назначены к конкретному процессорному ядру. У каждого ядра есть своя пара хипов и свои треды.

Представим, что на каком-то ядре баланс нарушился. Оно начало аллоцировать слишком много памяти и перестало помещаться в текущие сегменты. Рассмотрим, что может сделать система управления памятью в данном случае.

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

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

Когда текущий allocation context ядра заканчивается, а на соседнем ядре ничего в это время не происходит и проще туда перекинуть контекст, чем аллоцировать новый кусок памяти, то он вместе с этим контекстом вынужден туда перекинуть текущий поток. Теперь он будет работать на другом диапазоне памяти. Ему нужно, чтобы другой диапазон памяти находился в его кеше. Поэтому вместе с контекстом перемещается и текущий поток.

Какие выводы можно тут сделать. Во-первых, надо помнить как выделяется память. Во-вторых, чтобы оптимизировать работу GC, надо понимать, что вам понадобиться сколько-то объектов определенного типа. И после вызова new, заполнили память, алгоритм у вас отработал. А потом у вас создается еще один объект такого типа, например уже в цикле. И это плохо. Алгоритм отрабатывает некоторое время, в параллельных потоках в это время может быть произведена и другая аллокация. И в том же алгоритме вы можете выделять другую память. Соответственно, вы фрагментируете память по типу. Во время GC вам нужны объекты первого типа как объекты нулевого поколения, которые быстро исчезнут. Но в алгоритме вы аллоцируете вечные объекты. GC вынужден проводить фазу сжатия кучи, т.к. эти самые объекты, которые быстро ушли на покой, образовали маленькие участки свободной памяти, куда больше ничего не разместить. В противном случае GC мог бы обойтись обычным sweep: если бы участок был большой, он бы попал в список свободных участков.

Как лучше сделать. Если есть алгоритм на десять тысяч итераций, и там нужны объекты типа A, лучше выделить их сразу группой, а потом использовать. И когда GC отработает, у него получится огромный участок, где эти объекты были выделены. И он весь его отдаст в список свободных участков. Если же в вашем алгоритме вам каждый раз нужен всего лишь один экземпляр объекта типа A, то переиспользуйте его, сделайте метод init вместо конструктора. Получится, что вы вообще не фрагментируете память, а работаете на одном объекте.

Если вам нужно одновременно сто объектов делайте пул объектов, и доставайте их оттуда, чтобы не фрагментировать память короткоживущими объектами внутри алгоритма, чтобы не делать длительное сжатие кучи.

Первая книга вышла недавно. Ее написал Конрад Кокоса. Книга о менеджменте памяти объемом в тысячу страниц. Когда я ее в первый раз получил, я ожидал увидеть нечто другое. От размера тома у меня был шок. На самом деле автор молодец. Он написал ее для любого уровня подготовки. Это значит, можно вырезать вообще все, что лишнее. Например, уровень управления памятью процессором, Windows, список инструментов, которыми удобно пользоваться для диагностики - если вы хотите почитать просто про менеджмент памяти, то можно взять вторую половину книги. Первая половина книги - это просто подготовка ко второй. А вторая половина - как раз менеджмент памяти. Взять этот менеджмент памяти. Там очень легким языком все написано, постоянно делаются отсылки к предыдущим главам.

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

Вторая книга - Under the Hood of .NET Memory Management - для меня уже классика. Она не переведена на русский и распространяется только через сайт Red Gate, который делает разные инструменты для рефакторинга и работы с базами данных. В том числе у них есть тулза для анализа памяти, они были вынуждены исследовать как эта память работает. Там в очень коротком стиле, достаточном для того, чтобы приятно удивить людей на собеседовании в компанию, это все описано. В районе 200 страниц занимает. И моя книга.

Какие у вас вопросы возникли?

  • .... (вопрос не слышен на записи)

  • Получается надо. По картинкам частично Наталья ответит на этот вопрос, как правильно выкачивать это, чтобы лишнее не расходовать и чтобы все было удобно. Использовать надо в любом случае одни и те же массивы, использовать пулы, которые появились. Можно написать свои или новые, которые появились. Чтобы с этим грамотно работать можно использовать то, о чем расскажет Наталья.

  • ... (вопрос не слышен на записи)

  • Массив - это массив ссылок. Вы не можете сделать массив картинок. Массив ссылок на картинки.

  • ... (вопрос не слышен на записи)

  • Используйте это в качестве ключа GUI.

  • ... (вопрос не слышен на записи)

  • Вопрос о том, насколько логично перекидывать поток с ядра на ядро без учета использования памяти конкретным ядром. Какая получается классификация в данном случае.

У нас есть первое ядро. Оно что-то делает, за ним забито два потока. Когда на него перекидывается со второго ядра, то в рамках первого ядра все в порядке: оно как работало со своей памятью, так и работает. Это сценарий один. Сценарий номер два. Перекидываемый поток, со второго на первое. Когда мы это делаем, то мы его переносим не просто так, а потому, что он в данный момент активно аллоцирует память. Все это делается исходя из статистики. Из статистики, видимо, делается следующий вывод: приложение чаще работает с памятью, которую оно только что выделило.

  • ... (вопрос не слышен на записи)

  • На целевом ядре?

• ... (вопрос не слышен на записи) • Ничего не ляжет, там все в порядке будет. Они что-то считают, память не выделяют, контекст им не нужен. У первого ядра как были пара хипов, так они там и останутся. Они находятся в процессорном кеше. И когда туда переезжает тред с другого ядра и начинает аллоцировать, кеш не затронут. И с работой тоже самое.

• ... (вопрос не слышен на записи)

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

  • ...(вопрос не слышен на записи)

• Где посмотреть, как живет Linux? Если вы хотите посмотреть, как все тоже самое происходит в Linux, то можно обратиться к репорзиторию CoreCLR, там в разделе документации есть botr - Book of the Runtime. Это веб-книга, где разработчики ядра пишут документацию, как это работает. Это первый путь. Второй путь - это исходники. Но я не советую туда смотреть, если не хотите ужаснуться. Третий путь - это зайти в Book of the Runtime, посмотреть, кто туда пишет текст и связаться.

• ...(вопрос не слышен на записи)

  • Есть .NET стандарт, есть разные платформы. Как писать, чтобы все работало хорошо везде. Ответ такой. И .NET Core, и .NET framework работают на основе RED JIT 57:05. Это некая система джиттинга и GC. Кодовая база у них одна. Отличий мало. Например, в методе карточного стола есть отличия. В Windows проставление флагов как Bundle Table идет автоматически на основе триггера и со страницы памяти. В Linux это будет ручная простановка. Но вас это волновать не должно, это низкоуровневая подробность, которая ни на что не влияет. Основной слой, который влияет - унифицирован, а других особенностей я пока не встречал.

  • ...(вопрос не слышен на записи)

  • Да, этой литературы вполне достаточно.

-... (вопрос не слышен на записи)

  • Тут есть баланс. На счет того, что есть кеш.

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

  • ... (вопрос не слышен на записи)

  • Если они рядом хранятся, то это хорошо. Плохая ситуация, если у вас второе поколение большое и вы в разброс по всему поколению в каком-то алгоритме постоянно проставляете ссылки на младшее. Это редкая ситуация. Поэтому карточный стол и придуман. Приложение будет проставлять ссылки на младшее поколение из старшего в разброс по всей памяти. Если такое происходит - это очень плохо, потому что ему придется просматривать мегабайты объектов, в каких из них поля уходят на младшее поколение.

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

  • ... (вопрос не слышен на записи)

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

  • ... (вопрос не слышен на записи)

  • Если много кешей, то будет, скорее всего, несколько карточных столов. Когда у вас много не кешировано, а просто разреженная память и есть единичные ссылки, которые постоянно выставляются - это плохо. - ... (вопрос не слышен на записи)

  • Async/await вообще не предполагает знаний о том, на каких потоках это выполняется. Это синтаксический сахар. На счет синтаксического сахара в дальнейших докладах сделан вывод, что в нагруженных участках приложения, его использовать нельзя. Потому что синтаксический сахар порождает часто аллоцирование объектов там, где вы не увидите этого по тексту программы, это будет сопровождаться трафиком памяти, трафиком GC. Поэтому если у вас есть участки памяти, которые работают нагружено, там от синтаксического сахара придется отказаться.

  • ... (вопрос не слышен на записи)

  • А это является результатом синтаксического сахара. Потому что async/await, это все таки помощь нам писать код удобно. Надо чтобы нагруженный код, когда что-то выделяет, делал это в одном конкретном понятном месте, без использования дополнительных конструкций. Тогда вы контролируете ситуацию. Когда вы делаете async/await, forEach, лямбды, вы начинаете использовать дополнительные аллокации. Так создается дополнительный трафик. Это удобно, но в правильных пропорциях.