Упаковка значних типів С#. Що під капотом? - Блог ITVDN
ITVDN: курси програмування
Відеокурси з
програмування

    Обери свою IT спеціальність

    Підписка

    Обери свою IT спеціальність

    Підписка

      Упаковка значних типів С#. Що під капотом?

      advertisement advertisement

      Недавно у меня была действительно интересная дискуссия с участником команды, который является экспертом по системам типов в .NET. В ходе беседы он указал на интересный аспект упаковки типа значений в .NET при использовании ограничений. Заинтригованный этой дискуссией, я решил рассмотреть вопрос подробнее.

      Основы

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

      Предположим, у нас есть следующий код:

      Здесь нет ничего особенного. Foo - это структура, которая имеет целочисленное поле value. Она приватно реализует метод интерфейса, который пытается изменить значение этого поля, а также обычный метод, который делает то же самое.

      Теперь, если у нас есть следующий код:

      Какое правильное значение после вызова AddValue и после вызова Add?

      Если вы знакомы с языком, это, пожалуй, вовсе не удивит вас.

      Но давайте немного углубимся и посмотрим, как JIT это делает:

      Давайте сначала посмотрим на вызов AddValue.

      Обратите внимание, что я показываю код сборки x64, который гораздо проще понять. Первые 4 аргумента всегда передаются в регистре rcx, edx, r8, r9 (остаток передается через стек), а возвращаемое значение возвращается в rax. Все это 64-разрядные регистры. В приведенном выше коде JIT передает указатель «this» в rcx (указывает на часть стека, начиная с rbp-18h, и целое число 10 (0x0a) в rdx / edx (edx - это просто нижняя 32-битная часть rdx).

      Теперь, если вы посмотрите на фактический код Foo.AddValue:

      Не стесняйтесь игнорировать некоторые из отладочной тарабарщины (clr! JIT_DbgIsJustMyCode). Если вы следите за моими комментариями в сборке (начиная с ;), вы можете увидеть, что 10 добавляется в первую 4-байтовую ячейку памяти в 'this', что и должно делать value + = val.

      И вы получите следующее:

      Вызов интерфейса в методе экземпляра типа значения.

      Теперь давайте взглянем на вызов интерфейса - он немного усложняется:

      Опять-таки, я поставил комментарии по правой стороне кода сборки. Он в основном создает упакованную Foo, копирует значение во вновь созданную упакованную Foo. Обратите внимание, что смещение 8 для указателя MethodTable в начале объекта  имеют только те объекты и упакованные типы значений, которые являются объектами, разумеется. У обычных типов значений его нет.

      На данный момент проигнорируйте весь код отправки интерфейса (это не относится к нашему обсуждению), в конце концов вы придете к некоторым интересным инструкциям ниже:

      Этот код, на самом деле, мало что делает. Но он  дает нам  понимание того, как система работает в целом. Глядя на старый код, который мы показали ранее для метода AddValue, ожидаемо, что он укажет на первое поле. Однако все объекты для поддержки операций типа (таких как рефлексия, кастинг и т. д.) имеют свое первое поле размера указателя в качестве указателя типа, который в языке CLR называется MethodTable. Таким образом, CLR необходимо сгенерировать unboxing-заглушку, которая распаковывает запакованное значение и вызывает базовый JIT-метод, который предполагает работу с распакованным указателем this. Обратите внимание, что распаковка не включает копирование, она просто добавляет к ней смещение. Это фактически означает, что операция + = вступит в силу в упакованной копии. Однако поскольку упакованная Foo известна только компилятору, обновленное значение всегда теряется. И именно поэтому вы увидите:

      В случае наличия обобщений

      Теперь давайте добавим некоторые обобщения в микс:

      Несмотря на то, что это фантастический универсальный метод, сам вызов и лежащий в его основе код не имеет ничего удивительного. Как вы могли ожидать, несмотря на то, что вызывающий элемент передает Foo по ссылке, Add_WithoutConstraint создает его копию, прежде чем вызывает в IAdd, и модификация снова навсегда потеряна.

      Добавление ограничений

      Теперь интересный случай, о котором я хотел поговорить ранее в этой статье (спасибо, что остались со мной до сих пор!). Давайте создадим обобщенный метод с общим ограничением, где T является интерфейсом IAdd:

      Возможно, это не совсем очевидно для всех, foo.Add (val) - это вызов интерфейса с помощью инструкции callvirt: callvirt instance void IAdd :: Add (int32), потому что это единственный способ, который знает компилятор для вызова интерфейса.

      Интересной частью является то, что, когда мы вызываем Add_WithConstraints, вызов происходит точно таким же образом, за исключением того, что код, который мы вызываем, радикально отличается:

      Как вы видите, код удивительно прост. Без упаковки, без трансляции интерфейса и прямого вызова метода Foo.IAdd.Add. Никакое значение не теряется. И вы можете наблюдать побочный эффект:

      Причина в том, что компилятор теперь имеет достаточно информации, чтобы понять, что код для Foo и вызов интерфейса попадет именно на Foo.IAdd.Add, поэтому он пропускает формальности и вызывает функцию напрямую. Это как оптимизация производительности, но также с заметным побочным эффектом.

      Вывод

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

      Вы найдёте полный код в этом сообщении.

      Источник

      КОМЕНТАРІ ТА ОБГОВОРЕННЯ
      advertisement advertisement

      Купуй передплатуз доступом до всіх курсів та сервісів

      Бібліотека сучасних IT знань у зручному форматі

      Вибирай свій варіант підписки залежно від завдань, що стоять перед тобою. Але якщо потрібно пройти повне навчання з нуля до рівня фахівця, краще вибирати Базовий або Преміум. А для того, щоб вивчити 2-3 нові технології, або повторити знання, готуючись до співбесіди, підійде Пакет Стартовий.

      Стартовий
      • Усі відеокурси на 3 місяці
      • Тестування з 10 курсів
      • Перевірка 5 домашніх завдань
      • Консультація з тренером 30 хв
      59.99 $
      Придбати
      Преміум Plus
      • Усі відеокурси на 1 рік
      • Тестування з 24 курсів
      • Перевірка 20 домашніх завдань
      • Консультація з тренером 120 хв
      • Завантаження відео уроків
      120.00 $
      199.99 $
      Придбати
      Акція
      Базовий
      • Усі відеокурси на 6 місяців
      • Тестування з 16 курсів
      • Перевірка 10 домашніх завдань
      • Консультація з тренером 60 хв
      89.99 $
      Придбати
      Notification success
      Ми використовуємо cookie-файли, щоб зробити взаємодію з нашими веб-сайтами та послугами простою та значущою.