Любой инструмент для просмотра, где переменные или хранятся при выполнении программы .NET? это на стеке или куче?

С давних времен я хотел знать, где именно будет храниться переменная (будь то тип значения или ссылочный тип). Будет ли это на стеке или куче?

Я прочитал статью Эрика Лапперса на том же самом.

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

Спасибо

Ответ 1

Мысль о том, что хранилище делится на стек и кучу, является удобной абстракцией, которая будет вам хорошо служить. Но он намного более запутан, существует 6 различных хранилищ для переменных в .NET-программе.

Инструментом выбора здесь является отладчик, он может показать вам, где именно хранятся переменные. Это требует понимания того, как работает машинный код. Используйте Debug + Windows + Disassembly, чтобы увидеть машинный код. Также важно, чтобы вы просмотрели сборку Release вашей программы и изменили настройку, которая позволяет оптимизировать код даже при его отладке. Tools + Options, Debugging, General, отключите опцию "Подавить оптимизацию JIT при загрузке модуля". Теперь вы увидите машинный код так, как он будет выполняться на вашей пользовательской машине.

Вещи, которые вы должны знать заранее, чтобы понять все это:

  • Объекты ссылочного типа хранятся в куче GC. Переменная, которая хранит ссылку, имеет такие же варианты хранения, как значения типа значения.

  • Значения типа значений или ссылки на объекты имеют шесть возможных мест хранения:

    • Они хранятся в куче GC, если переменная является членом ссылочного типа
    • Они сохраняются в куче загрузчика AppDomain, если переменная объявлена ​​static
    • Они хранятся в потоковом локальном хранилище, если переменная [ThreadStatic]
    • Они могут храниться в фрейме стека, если переменная является аргументом метода или локальной переменной
    • Они могут храниться в регистре CPU, если переменная является аргументом метода или локальной переменной
    • Специфика джиттера x86, переменная типа Single или Double может быть сохранена в стеке FPU.

Последние три пули - это место, где он становится сложным и почему вам нужно посмотреть машинный код, чтобы узнать, где они хранятся. Это особая реализация, характер джиттера. И сильно зависит от того, включен ли оптимизатор дрожания. Сделать правильный выбор здесь очень важно для perf. Грубая схема (пропуская джиттер ARM):

  • Первые два аргумента метода хранятся в регистры CPU для джиттера x86, включая значение этого для методов экземпляра. Джиттер x64 использует 4 регистра. Регистры процессора с плавающей запятой используются для передачи переменных типа Single и Double на x86, регистры XMM на x64

  • Возвращаемое значение функции возвращается в регистре CPU, если оно подходит, используя регистр EAX или RAX, ST0, если это значение с плавающей запятой. Если он не подходит, то зарезервированное пространство вызывающего абонента в кадре стека для значения и передало ему указатель

  • Оптимизатор джиттера ищет возможности для хранения локальных переменных в регистре CPU. Он может пропустить регистр обратно в стек стека, если он вынужден сделать это, потому что он не зарегистрирован.

Существует ряд наблюдаемых побочных эффектов этих деталей реализации:

  • Получение локальных переменных, хранящихся в Cpu-регистрах, затрудняет отладку кода. Отладчик не знает достаточно о месте хранения. Это основная причина существования сборки Debug, она подавляет оптимизацию, поэтому вы можете легко проверить локальные переменные, отладчик знает слот фрейма стека, используемый для переменной
  • Вы не можете проверить возвращаемое значение метода, что является существенным неудобством при отладке. Отладчик не знает достаточно о месте хранения, выбранном дрожанием, чтобы надежно найти значение. EDIT: исправлено в VS2013
  • Вам может быть сложно отладить проблемы с потоками из-за того, что переменная, оптимизированная для хранения в регистре процессора. Тестирование значения в цикле или оператор if() дает копию значения в регистре, а не значение, сохраненное в памяти. В частности, проблема с джиттером x86 и причиной ключевого слова volatile - ключевое слово, которое подавляет эту оптимизацию.
  • Вы можете инициализировать указатель на локальную переменную, не связывая ее. В отличие от переменных, хранящихся в куче GC, которые могут быть перемещены сборкой мусора и, следовательно, требуют фиксации, локальные переменные имеют фиксированный адрес хранилища, действующий на всем протяжении тела метода
  • Объем пространства, выделенного для кадра стека, определяется дрожанием. Однако можно выделить кусок самостоятельно, ключевое слово С# stackalloc поддерживает его. Это самая быстрая память, которую вы можете выделить напрямую.
  • Наличие значений с плавающей запятой, хранящихся в регистре FPU, вызывает проблемы с плавающей запятой. Пока он хранится в FPU, значение сохраняется с точностью до 80 бит. Но когда он проливается на память, он усекается до 32 или 64 бит. Непредсказуемость возникновения этого разлива (плюс другая стратегия джиттера x64) дает результаты с плавающей запятой, которые могут быть разными с небольшими изменениями, приводящими к большим различиям в результате вычисления, если вычисление теряет много значительных цифр.