Pull to refresh

Детали реализации стека — часть первая

Reading time3 min
Views28K
Original author: Eric Lippert
image
Какое-то время назад я писал о том, что «ссылки» — это не «адреса», когда речь идёт о C# и размещении его объектов в памяти. Хотя это действительно так, но это всего лишь деталь реализации, но не смысл «ссылки». Другая деталь реализации, которую часто путают с сутью — это то, что «память под значимые типы (value types) выделяется на стеке». Я часто это вижу, потому что именно так написано в нашей документации.

Практически каждая статья, которую я вижу, подробно описывает (часто неверно) что такое стек и что основное различие между значимыми и ссылочными типами — это то, что значимые типы располагаются на стеке. Я уверен Вы можете найти множество примеров таких статей в сети.


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

К сожалению документация не сфокусирована на наиболее значимых характеристиках, но сфокусирована на деталях реализации и упускает суть значимых типов. Я бы очень хотел, чтобы все те статьи, которые объясняют, «что такое стек» вместо этого объясняли бы что такое «копирование по значению» и как непонимание этого механизма может стать причиной ошибок.

Утверждение о том, что значимые типы располагаются на стеке в общем случае не верно. В документации на MSDN правильно замечено, что значимые типы располагаются на стеке иногда. Например, поле типа int в ссылочном типе — это часть объекта этого типа и, как и весь объект его поле расположено в куче. Та же история с локальными переменными, которые попадают в замыкание анонимных методов (*), потому что они по сути становятся полями скрытого класса и тоже располагаются в куче, так что локальные переменные могут располагаться в куче даже если они значимого типа.

Короче говоря, мы имеем объяснение, которое ничего не объясняет. Отбросив соображения производительности что ещё, с точки зрения ожиданий разработчика может заставить CLRjitter разместить переменную типа int на стеке, а не в куче? Ничего, пока не нарушается спецификация система может выбирать наиболее эффективную стратегию генерирования кода.

Ага, никто не обещал, что операционная система поверх которой реализован CLI предоставляет массив размером в 1 мегабайт под названием «стек». Windows обычно делает это и этот одно мегабайтный массив отличное место для того, чтобы эффективно хранить небольшие объекты с коротким временем жизни, но нет никаких требований или гарантий, что операционная система предоставляет такого рода структуру или что jitter будет её использовать. В принципе Jitter может решить создавать все локальные переменные в куче не смотря на потерю производительности, это будет работать пока семантические требования к значимым типам выполняются.

Ещё хуже думать, что значимые типы «быстрые и маленькие» а ссылочные «большие и медленные». Действительно, значимые типы могут быть сгенерированные jitter-ом в код на стеке, который очень быстр как при выделении, так и при очистке памяти. Но при этом большие структуры, создаваемые на куче, такие как массив элементов значимого типа, тоже создаются очень быстро при условии, что они инициализируются значениями по умолчанию. И ссылочные типы занимают дополнительное место в памяти. И конечно существуют такие условия, когда значимые типа дают большой выигрыш в производительности. Но в подавляющем большинстве программ то, как локальные переменные создаются и уничтожаются не может быть узким местом с точки зрения производительности.

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

(*) справедливо и для блока итератора.

От переводчика: в последнее время мне снова выпала возможность провести несколько собеседований и часто на вопрос «что такое значимые и ссылочные типы» я слышал «значимые типы — это типы, экземпляры которых располагаются на стеке, а ссылочные — это типы, экземпляры которых располагаются в куче». Так что перед Вами перевод очень старой, но не потерявшей свою актуальность статье Eric-а Lippert-а. Я постарался сделать перевод как можно более читаемым и лёгким для восприятия на Русском языке, так что он существенно отличается от оригинала по форме, но не по смыслу.
Tags:
Hubs:
Total votes 40: ↑34 and ↓6+28
Comments24

Articles