Обновить

Комментарии 42

НЛО прилетело и опубликовало эту надпись здесь
Boxing выделяет память в куче, unboxing в стеке потока выполнения. Смысл статьи в первом приближении сравнить производительность данных операций в рамках .NET.
НЛО прилетело и опубликовало эту надпись здесь
Я мог неправильно вас понять, но мне сложно представить почему вы считаете что память под стек уже выделена, а в куче нет. Аналогично стеку память выделяется и под процесс на момент инициализации GC и запуска процесса.

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

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


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


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

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

НЛО прилетело и опубликовало эту надпись здесь

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


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

НЛО прилетело и опубликовало эту надпись здесь

В общем смысле вы правы. 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 :)

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

Хотел тоже самое написать, но побоялся словить кучу минусов за первый коммент с негативным содержанием)))
НЛО прилетело и опубликовало эту надпись здесь
Во истину, даже Рихтер акцентировал внимание на этом в известной книжке, которую и так все читали.
Вспоминая про то, что за выделение памяти в .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, так что если будет вопрос, как всё заведется на другом процессоре — я смогу проверить и оценить. А по книге — нет.

НЛО прилетело и опубликовало эту надпись здесь

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


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


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


  • Есть еще один пример использования BanchmarkDotNet от DreamWalker
  • Если потребуется ответ на вопрос, заданный в начале статьи, я её найду в интернете, возьму код и проверю на заданном процессоре
НЛО прилетело и опубликовало эту надпись здесь

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


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

НЛО прилетело и опубликовало эту надпись здесь

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


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

НЛО прилетело и опубликовало эту надпись здесь
Смысл этой статьи как всегда в комментариях)
Для проверки этого утверждения я набросал 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'ов, к примеру, вынести не получается).
НЛО прилетело и опубликовало эту надпись здесь
Boxing и unboxing — что быстрее? Рихтер ответил на этот вопрос еще несколько лет назад! Рекомендую почитать CLR via C# прежде чем выдумывать свой велосипед. Я извиняюсь но ваша статья ни о чем
Что бы понять, что быстрее, Рихтер не нужен. Нужно просто понимание сложности алгоритмов в одном и другом случае.
Все забыли что при боксинге еще нужно Method Table создать и присобачить к результату.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации