Подтвердить что ты не робот

Почему методы экземпляра С# struct, вызывающие методы экземпляра в поле структуры, сначала проверяют ecx?

Почему X86 для следующего метода С# CallViaStruct включает инструкцию cmp?

struct Struct {
    public void NoOp() { }
}
struct StructDisptach {

    Struct m_struct;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void CallViaStruct() {
        m_struct.NoOp();
        //push        ebp  
        //mov         ebp,esp  
        //cmp         byte ptr [ecx],al  
        //pop         ebp  
        //ret
    }
}

Вот более полная программа, которая может быть скомпилирована с различными (выпускными) декомпиляциями в виде комментариев. Я ожидал, что X86 для CallViaStruct в типах ClassDispatch и StructDispatch будет одинаковым, но версия в StructDispatch (извлеченная выше) включает в себя инструкцию cmp, а другая - нет.

Кажется, инструкция cmp - это идиома, используемая для обеспечения того, что переменная не равна null; разыменование регистра со значением 0 запускает av, который превращается в NullReferenceException. Однако в StructDisptach.CallViaStruct я не могу представить способ для ecx быть пустым, если он указывает на структуру.

UPDATE: ответ, который я хочу принять, будет включать код, который вызывает NRE, который должен быть сброшен StructDisptach.CallViaStruct, если он cmp разыменовал команду с нулевым ecx регистром. Обратите внимание, что это легко сделать с любым из методов CallViaClass, установив m_class = null и сделать невозможным с ClassDisptach.CallViaStruct, поскольку нет инструкции cmp.

using System.Runtime.CompilerServices;

namespace NativeImageTest {

    struct Struct {
        public void NoOp() { }
    }

    class Class {
        public void NoOp() { }
    }

    class ClassDisptach {

        Class m_class;
        Struct m_struct;

        internal ClassDisptach(Class cls) {
            m_class = cls;
            m_struct = new Struct();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaClass() {
            m_class.NoOp();
            //push        ebp  
            //mov         ebp,esp  
            //mov         eax,dword ptr [ecx+4]  
            //cmp         byte ptr [eax],al  
            //pop         ebp  
            //ret  
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaStruct() {
            m_struct.NoOp();
            //push        ebp
            //mov         ebp,esp
            //pop         ebp
            //ret
        }
    }

    struct StructDisptach {

        Class m_class;
        Struct m_struct;

        internal StructDisptach(Class cls) {
            m_class = cls;
            m_struct = new Struct();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaClass() {
            m_class.NoOp();
            //push        ebp  
            //mov         ebp,esp  
            //mov         eax,dword ptr [ecx]  
            //cmp         byte ptr [eax],al  
            //pop         ebp  
            //ret  
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaStruct() {
            m_struct.NoOp();
            //push        ebp  
            //mov         ebp,esp  
            //cmp         byte ptr [ecx],al  
            //pop         ebp  
            //ret  
        }
    }

    class Program {
        static void Main(string[] args) {
            var classDispatch = new ClassDisptach(new Class());
            classDispatch.CallViaClass();
            classDispatch.CallViaStruct();

            var structDispatch = new StructDisptach(new Class());
            structDispatch.CallViaClass();
            structDispatch.CallViaStruct();
        }
    }
}

UPDATE: позволяет использовать callvirt для не виртуальной функции, которая имеет побочный эффект от нулевой проверки этого указателя. Хотя это имеет место для CallViaClass callsite (именно поэтому мы видим нулевую проверку там) StructDispatch.CallViaStruct использует инструкцию call.

.method public hidebysig instance void  CallViaClass() cil managed noinlining
{
  // Code size       12 (0xc)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldfld      class NativeImageTest.Class NativeImageTest.StructDisptach::m_class
  IL_0006:  callvirt   instance void NativeImageTest.Class::NoOp()
  IL_000b:  ret
} // end of method StructDisptach::CallViaClass

.method public hidebysig instance void  CallViaStruct() cil managed noinlining
{
  // Code size       12 (0xc)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldflda     valuetype NativeImageTest.Struct NativeImageTest.StructDisptach::m_struct
  IL_0006:  call       instance void NativeImageTest.Struct::NoOp()
  IL_000b:  ret
} // end of method StructDisptach::CallViaStruct

UPDATE: было высказано предположение, что cmp может быть ловушкой для случая, когда a null этот указатель не был захвачен на сайте вызова. Если бы это было так, я бы ожидал, что cmp произойдет один раз в верхней части метода. Однако он появляется один раз для каждого вызова NoOp:

struct StructDisptach {

    Struct m_struct;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void CallViaStruct() {
        m_struct.NoOp();
        m_struct.NoOp();
        //push        ebp  
        //mov         ebp,esp  
        //cmp         byte ptr [ecx],al  
        //cmp         byte ptr [ecx],al  
        //pop         ebp  
        //ret  
    }
}
4b9b3361

Ответ 1

Короткий ответ: JITTER не может доказать, что на конструкцию не ссылается указатель, и по крайней мере разыменовать по крайней мере один раз при каждом вызове NoOp() для правильного поведения.


Длинный ответ: Структуры странные.

JITTER является консервативным. Там, где это возможно, он может только оптимизировать код таким образом, чтобы он мог быть абсолютно уверен в правильном поведении. "В основном правильно" недостаточно.

Итак, вот пример сценария, который сломался бы, если JITter оптимизировал бы разыменование. Рассмотрим следующие факты:

Во-первых: Помните, что structs (и do!) существуют вне С# - указатель на StructDispatch может поступать из неуправляемого кода, например. Как заметил Лукас, вы можете использовать указатели для обмана; но JITTER не может точно знать, что вы не используете указатели на StructDispatch где-то еще в коде.

Второе: помните, что в неуправляемом коде, который является самой большой причиной существования структур, в первую очередь, все ставки отключены. Просто потому, что вы просто прочитали значение из памяти, это не значит, что он будет тем же самым значением или даже будет значением при следующем чтении того же точного адреса. Threading и многопроцессорность могут буквально изменить что-то значение на следующем такте, не говоря уже о неактивных актерах, таких как DMA. Параллельный поток мог VirtualFree() страницу, содержащую эту структуру, и JITTER должен защищать ее. Вы просили прочитать по памяти, чтобы вы читали из памяти. Я предполагаю, что если вы воспользовались оптимизатором, он удалит одну из этих команд cmp, но я очень сомневаюсь, что он удалит оба.

В-третьих: Исключения также являются реальным кодом. NullReferenceException не обязательно останавливает программу; его можно поймать и обработать. Это означает, что с точки зрения JITTER NRE больше похожа на оператор if, чем на goto: это своего рода ветвь условия, которую нужно обрабатывать и рассматривать при каждом разыменовании памяти.

Итак, теперь соедините эти части.

JITter не знает - и не может знать, что вы не используете небезопасный С# или внешний источник в другом месте для взаимодействия с памятью StructDispatch. Он не создает отдельные реализации CallViaStruct(), один для "вероятно безопасного кода С#" и один для "возможно рискованного внешнего кода"; он всегда производит консервативную версию для возможных рискованных сценариев. Это означает, что он не может просто полностью отключить вызовы NoOp(), потому что нет гарантии, что StructDispatch, скажем, не отображается на адрес, который даже не выгружен в память.

Он знает, что NoOp() пуст и может быть удален (вызов может уйти), но он, по крайней мере, должен имитировать ldfla, выталкивая адрес памяти структуры, потому что может быть код в зависимости от того, что NRE поднимается. Разделения памяти похожи на if-statements: они могут вызывать ветку, и неспособность вызвать ветвь может привести к нарушенной программе. Microsoft не может делать предположения и просто сказать: "Ваш код не должен полагаться на это". Представьте сердитый телефонный звонок в Microsoft, если NRE не был записан в журнал бизнес-ошибок только потому, что JITTER решил, что это не было "достаточно важным" NRE для запуска в первую очередь. У JITter нет выбора, кроме как разыменовать этот адрес хотя бы один раз, чтобы обеспечить правильную семантику.


В классах нет ни одной из этих проблем; там нет принудительной памяти странности с классом. Но структуры, тем не менее, более quirkier.