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

Какое преимущество заключается в сохранении "this" в локальной переменной в методе struct?

Я просматривал исходное дерево .NET Core сегодня и перебегал этот шаблон в System.Collections.Immutable.ImmutableArray<T>:

T IList<T>.this[int index]
{
    get
    {
        var self = this;
        self.ThrowInvalidOperationIfNotInitialized();
        return self[index];
    }
    set { throw new NotSupportedException(); }
}

Этот шаблон (сохранение this в локальной переменной), как представляется, последовательно применяется в этом файле всякий раз, когда this в противном случае будет ссылаться несколько раз в том же методе, но не в том случае, когда он ссылается только один раз. Поэтому я начал думать о том, какие относительные преимущества могут быть сделаны таким образом; мне кажется, что преимущество, скорее всего, связано с производительностью, поэтому я пошел по этому пути немного дальше... может быть, я забываю что-то еще.

CIL, который испускается для "store this в локальном" шаблоне, кажется, выглядит примерно как ldarg.0, затем ldobj UnderlyingType, затем stloc.0, чтобы более поздние ссылки исходили из ldloc.0 вместо голого ldarg.0, как это было бы просто использовать this несколько раз.

Возможно, ldarg.0 значительно медленнее, чем ldloc.0, но недостаточно для перевода С# -to-CIL или JITter, чтобы искать возможности для оптимизации этого для нас, так что имеет смысл писать это странный образ в коде С# в любое время, когда мы в противном случае испустили бы две инструкции ldarg.0 в методе экземпляра структуры?

Обновление: или, вы знаете, я мог бы взглянуть на комментарии в верхней части этого файла, которые объяснить, что именно происходит...

4b9b3361

Ответ 1

Как вы уже заметили, System.Collections.Immutable.ImmutableArray <T> является структурой:

public partial struct ImmutableArray<T> : ...
{
    ...

    T IList<T>.this[int index]
    {
        get
        {
            var self = this;
            self.ThrowInvalidOperationIfNotInitialized();
            return self[index];
        }
        set { throw new NotSupportedException(); }
    }

    ...

var self = this; создает копию структуры, на которую ссылается. Зачем это нужно делать? исходные комментарии этой структуры дают объяснение, почему это необходимо:

///Этот тип должен быть потокобезопасным. Как структура, он не может защитить свои собственные поля
///из одного потока, когда его члены выполняются в других потоках
///, потому что структуры могут меняться вместо просто переназначения поля, содержащего
///эта структура. Поэтому чрезвычайно важно, чтобы
///** Каждый член должен только разыгрывать это ОДИН РАЗ. **
///Если член должен ссылаться на поле массива, это считается разыменованием этого.
///Вызов других членов экземпляра (свойств или методов) также считается разыменованием этого.
///Любой член, который должен использовать это более одного раза, должен вместо этого
///присваиваем это локальной переменной и вместо этого используем ее для остальной части кода.
///Это эффективно копирует одно поле в структуре в локальную переменную, чтобы
///он изолирован от других потоков.

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

Обновление: также прочитайте ответ supercats, в котором подробно объясняется, какие условия должны выполняться, чтобы операция вроде создания локальной копии структуры (т.е. var self = this;) является потокобезопасным, и что может произойти, если эти условия не выполняются.

Ответ 2

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

Таким образом, если у вас была структура:

struct foo {
  String x;
  override String ToString() {
    String result = x;
    System.Threading.Thread.Sleep(2000);
    return result & "+" & x;
  }
  foo(String xx) { x = xx; }
}

и один из них должен был вызвать следующий метод для двух потоков с тем же массивом myFoos типа foo[]:

myFoos[0] = new foo(DateTime.Now.ToString());
var st = myFoos[0].ToString();

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

Обратите внимание, что для структур, которые содержат поле типа Int64, UInt64 или Double, или которые содержат более одного поля, возможно, что оператор типа var temp=this;, который встречается в одном потоке while другой поток переписывает место, где хранился this, может закончиться копированием структуры, которая содержит произвольную смесь старого и нового содержимого. Только если структура содержит одно поле ссылочного типа или одно поле из 32-битного или меньшего примитива, гарантируется, что чтение, которое происходит одновременно с записью, даст некоторое значение, которое фактически было выполнено структурой, и даже это может иметь некоторые причуды (например, по крайней мере, в VB.NET, оператор вроде someField = New foo("george") может очистить someField до вызова конструктора).