Comments 42
В таком случае обе эти операции лишь копируют свои значения, т.к. память под их нужды была выделена заранее.
Память на стеке уже выделена, потому что когда метод стартует он уже знает, сколько у него локальных переменных и для них всех уже есть место на стеке.
Зачем лишний раз выделять память на стеке когда размер итак известен в момент компиляции.
Динамическая она потому что меняеется во время программы, а вот информация о размере всех переменных метода на стеке есть на момент компиляции.
Память на стеке выделяеть только stackallock
и то в unsafe
режиме.
Даже более того. Так как это стандратная операция про выделения места для локальных переменных. В ассемблере ввели команду enter
которая правильно выделяет память на стеке на старте метода.
И нет смысла лишний раз что-то выделять когда это можно сделать один раз.
Да, golang так делает например. Но мы же про C# сейчас :)
А расскажите пожалуйста когда анбоксин происходит не на стек? Я действительно не знаю таких случаев.
Во всех случая, что я знаю, анбоксин всегда проходит через стек.
В общем смысле вы правы. JIT действительно может сделать много оптимизаций включая анбоксинг в регистр. Но это уже то как JIT решит.
Сам по себе IL (промежуточный язык) это стекавая машина, т. е. в IL весь анбоксинг всегда проходит через стек, особенность без регистровой виртуальной машины, а вот уже потом JIT может замаппить это на регистры.
В общем проверил, да 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
даже не переиспользовал место в зависимости от ветвлений, хотя точно мог это сделать, кто его знает какие оптимизации он делает.
Если увеличить размер первой структуру то и место в стеке резервируется большее.
В общем магии нет, и выделения на стеке не происходит динамически, а фиксированно и известно в самом методе при его старте в виде константы.
В вашем примере в стеке память выделяется один раз на тест, а в куче — один раз на каждую итерацию цикла. Конец немного предсказуем.
Вспоминая про то, что за выделение памяти в .NET в управляемой куче отвечает сборщик мусора (Garbage Collector) важно отметить, что делает он это нелинейно, ввиду возможной её фрагментации (наличия свободных участков памяти) и поиска необходимого свободного участка требуемого размера.
Возможно ошибаюсь, но выделение памяти как раз работает очень быстро. Вомжно быстрее, чем в С/С++. В С/С++ действительно ОС должна выделять свободные страницы и искать повсюду (опустим оптимизации). В случае с CLR, память выделяется большими сегментами (одна затратная операция из того же С/С++ — VirtualAlloc), но затем CLR хранит указатель (Next Object) на следующий свободный кусок и никогда не возвращается к пробелам, а быстро выдает нужный кусок, если он в сегменте остался, иначе снова выделяет большой сегмент. После выделения под инстанс указатель смещается и все, никаких возвратов. Это описано в Under the Hood of .NET Memory Management
А фрагментация — проблема для LOH, т.к. копировать большие объекты при компактификации затратно. В случае же с SOH — есть компактификация, т.е. после того как сегмент обрабатывается GC, он ничего не делает с фрагментацией, он просто копирует выжившие объекты друг за другом. Но в целом трудно себе представить структуру (value type), которая поедет в боксинг, и вряд ли это проблема для структур.
Я представлял себе ситуацию, когда у нас выделяется память под массив фикс. размера (например List), который затем выходит за границы, следствием чего, вероятно, является выделение нового участка памяти его копирование + очистка старого. Судя по всему, изначальный участок памяти не будет повторно переиспользован до компактификации.
Спасибо за исправления.
А вот тут я полностью на стороне fsou11.
При разработке не так сильно важна теория, как практика.
По этой статье можно уже оценивать, насколько код будет работать быстрее/медленнее после оптимизаций. А по Рихтеру — нет.
И плюс есть нормальный открытый код на GitHub, так что если будет вопрос, как всё заведется на другом процессоре — я смогу проверить и оценить. А по книге — нет.
Нет, не будешь. Пока всё быстро работает — нет смысла, даже глупо как-то.
Зато если вдруг в задаче станет вопрос: что лучше — больше боксинга или же анбоксинга (и при условии, что это не преждевременная оптимизация), то тут статья поможет.
Я вот прочитал статью и понял, что:
- Есть еще один пример использования BanchmarkDotNet от DreamWalker
- Если потребуется ответ на вопрос, заданный в начале статьи, я её найду в интернете, возьму код и проверю на заданном процессоре
Нет, не понял. Я прямо пишу свои аргументы, а не скрываюсь за мнимой мудростью.
Я написал, почему я считаю, что наличие этой статьи на хабре лучше, чем отсутствие. А какие твои аргументы?
Для проверки этого утверждения я набросал 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 — что быстрее?