Pull to refresh

Comments 42

UFO just landed and posted this here
Boxing выделяет память в куче, unboxing в стеке потока выполнения. Смысл статьи в первом приближении сравнить производительность данных операций в рамках .NET.
UFO just landed and posted this here
Я мог неправильно вас понять, но мне сложно представить почему вы считаете что память под стек уже выделена, а в куче нет. Аналогично стеку память выделяется и под процесс на момент инициализации GC и запуска процесса.

В таком случае обе эти операции лишь копируют свои значения, т.к. память под их нужды была выделена заранее.
UFO just landed and posted this here

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


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


Память на стеке выделяеть только stackallock и то в unsafe режиме.

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

UFO just landed and posted this here

Да, golang так делает например. Но мы же про C# сейчас :)


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

UFO just landed and posted this here

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

А что происходит здесь?
int[] ints = new int[1];
object i = 1;
ints[0] = (int)i;

ints[0] — это адрес в куче. Здесь распаковка тоже через стек?

В общем проверил, да JIT заоптимизировал в регистр:



Первый парамет тоже всегда через ecx приходит, так что стек используется только для бекапа регистов и то только 2-х регистров.
Но в IL все ещё стек потому что регистров нет, но и локальныз перменных нет :)

Память выделяется даже под переменные, использование которых зависит от аргументов вызова метода?

public void Execute(bool exist) {
  if(exist) {
    SmallUserStruct a = new SmallUserStruct();
  } else {
    BigUserStruct b = new BigUserStruct();
  }
}

Конечно! Только в данном случае это будет разделяемая память. Т.е. выделится один кусок стека для одной ветки в нем будет a, для другой b. Ну и это уже на совести jit'а. Может выделить место и для обоих структур.
Вы поймите, что зарезервировать стек один раз на число просчитанное компилятор быстрее и проще, чем делать это динамически в рантайме.
Стек всегда двигается то вверх, то вниз. И управлять им соответственно просто, это вам не динамическая память. Поэтому все и стараются уменьшить работу с ним до минимума для оптимизации.
Буду за компом посмотрю ассемблерный и il-овский код.

Я проверил, листинги ассемблера большие даже в релизе, поэтому приводить не буду, там даже конструкторы заинлайнились. Добавил конструктор, чтобы он меньше оптимизировал, так JIT его всё равно заинлайнил. В общем, по IL коду сразу видно, что независимо от ветвлений, все локальные переменные объявляются сразу в заголовке, что логично:



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


Спасибо, надо освежать знания по memory management :)

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

Хотел тоже самое написать, но побоялся словить кучу минусов за первый коммент с негативным содержанием)))
UFO just landed and posted this here
Во истину, даже Рихтер акцентировал внимание на этом в известной книжке, которую и так все читали.
Вспоминая про то, что за выделение памяти в .NET в управляемой куче отвечает сборщик мусора (Garbage Collector) важно отметить, что делает он это нелинейно, ввиду возможной её фрагментации (наличия свободных участков памяти) и поиска необходимого свободного участка требуемого размера.

Возможно ошибаюсь, но выделение памяти как раз работает очень быстро. Вомжно быстрее, чем в С/С++. В С/С++ действительно ОС должна выделять свободные страницы и искать повсюду (опустим оптимизации). В случае с CLR, память выделяется большими сегментами (одна затратная операция из того же С/С++ — VirtualAlloc), но затем CLR хранит указатель (Next Object) на следующий свободный кусок и никогда не возвращается к пробелам, а быстро выдает нужный кусок, если он в сегменте остался, иначе снова выделяет большой сегмент. После выделения под инстанс указатель смещается и все, никаких возвратов. Это описано в Under the Hood of .NET Memory Management
А фрагментация — проблема для LOH, т.к. копировать большие объекты при компактификации затратно. В случае же с SOH — есть компактификация, т.е. после того как сегмент обрабатывается GC, он ничего не делает с фрагментацией, он просто копирует выжившие объекты друг за другом. Но в целом трудно себе представить структуру (value type), которая поедет в боксинг, и вряд ли это проблема для структур.
Имел ввиду структуру, которая забоксится в LOH.
Похоже вы действительно правы касаемо инкриминирующего указателя в куче, работающему аналогично стеку и отсутствия такого понятия, как возврат к пробелам в фрагментированной памяти.

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

Спасибо за исправления.
У рихтера написано абсолютно тоже самое… упаковка медленее, чем распаковка… в чем смысл статьи? проверить не врет ли он? =)))
Всё верно. Проверить утверждение самому и предложить поделиться своими результатами читателей для оценки различий в зависимости от окружения, на котором данные benchmark'и запускаются.

А вот тут я полностью на стороне fsou11.
При разработке не так сильно важна теория, как практика.


По этой статье можно уже оценивать, насколько код будет работать быстрее/медленнее после оптимизаций. А по Рихтеру — нет.


И плюс есть нормальный открытый код на GitHub, так что если будет вопрос, как всё заведется на другом процессоре — я смогу проверить и оценить. А по книге — нет.

UFO just landed and posted this here

Нет, не будешь. Пока всё быстро работает — нет смысла, даже глупо как-то.


Зато если вдруг в задаче станет вопрос: что лучше — больше боксинга или же анбоксинга (и при условии, что это не преждевременная оптимизация), то тут статья поможет.


Я вот прочитал статью и понял, что:


  • Есть еще один пример использования BanchmarkDotNet от DreamWalker
  • Если потребуется ответ на вопрос, заданный в начале статьи, я её найду в интернете, возьму код и проверю на заданном процессоре
UFO just landed and posted this here

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


Я написал, почему я считаю, что наличие этой статьи на хабре лучше, чем отсутствие. А какие твои аргументы?

UFO just landed and posted this here

Наиболее вероятное использование:
Распаковать структуру, поработать с ней, упаковать обратно при необходимости.
Используется в тех же фреймворках.


При этом MC++ умеет работать с упакованной структурой напрямую без копирования на стек в отличие от C#, что положительно сказывается на скорости.

UFO just landed and posted this here
Смысл этой статьи как всегда в комментариях)
Для проверки этого утверждения я набросал 4 небольшие функции: 2 для boxing и 2 для unboxing типов int и struct.

int, внезапно, это тоже struct. Смысл тестировать с еще одной структурой?


Для замера производительности была использована библиотека BenchmarkDotNet в режиме Release (буду рад если DreamWalker подскажет, каким образом сделать данные замеры более объективными).

Я не DreamWalker, но вот http://benchmarkdotnet.org/RulesOfBenchmarking.htm

int, внезапно, это тоже struct. Смысл тестировать с еще одной структурой?

Это действительно оказалось внезапным, однако хоть int и является структурой, он не является user defined struct.

Я не DreamWalker, но вот http://benchmarkdotnet.org/RulesOfBenchmarking.htm

Все эти рекомендации были учтены в момент измерения.

Меня интересует скорее проблема того, что в методах присутствует начальная инициализация, которую я старался нивелировать за счёт большего кол-ва прогонов внутри метода. Связано это с тем, что библиотека не позволяет использовать в benchmark'ах методы, которые имеют аргументы (а следовательно, начальную инициализацию value type'ов, к примеру, вынести не получается).
UFO just landed and posted this here
Boxing и unboxing — что быстрее? Рихтер ответил на этот вопрос еще несколько лет назад! Рекомендую почитать CLR via C# прежде чем выдумывать свой велосипед. Я извиняюсь но ваша статья ни о чем
Что бы понять, что быстрее, Рихтер не нужен. Нужно просто понимание сложности алгоритмов в одном и другом случае.
Все забыли что при боксинге еще нужно Method Table создать и присобачить к результату.
Sign up to leave a comment.

Articles