存储"这个"有什么好处?在struct方法的局部变量中?

时间:2015-06-20 17:59:22

标签: c# .net

我今天正在浏览.NET Core源代码树并在System.Collections.Immutable.ImmutableArray<T>中遇到this pattern

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

这种模式(将this存储在局部变量中)似乎在此文件中始终如一地应用this,否则将在同一方法中多次引用,但不会在仅引用一次时。所以我开始思考这样做的相对优势是什么;在我看来,这种优势很可能与性能有关,所以我走得更远了......也许我忽略了别的东西。

{&#34;商店this在本地&#34;中发出的CIL模式似乎看起来像ldarg.0,然后是ldobj UnderlyingType,然后是stloc.0,以便后来的引用来自ldloc.0,而不是像ldarg.0那样的this只需多次使用ldarg.0

可能ldloc.0明显慢于ldarg.0,但C#-to-CIL转换或JITter找不到为我们优化此功能的机会都不够,这样它就能做得更多感觉在C#代码中编写这种奇怪的模式,否则我们会在结构实例方法中发出两个/[1-4[1-7][1-6][1-8]{4}/指令?

更新:或者,您知道,我可以查看该文件顶部的评论,explain确切地发生了什么......

2 个答案:

答案 0 :(得分:20)

正如您已经注意到的,System.Collections.Immutable.ImmutableArray&lt; T&gt;是一个结构

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;创建 this 引用的结构的副本。为什么要这样做呢? source comments of this struct解释了为什么有必要:

  

///这种类型应该是线程安全的。作为一个结构,它无法保护它   自己的领域   ///从其中一个线程更改而其成员在其他线程上执行时   ///因为结构可以简单地通过重新分配包含
的字段来改变到位   ///这个结构。因此,非常重要的是   /// **每个成员都应该只取消引用此ONCE。 **
  ///如果一个成员需要引用数组字段,那就算是对它的解引用了   ///调用其他实例成员(属性或方法)也算作取消引用它。
  ///任何需要多次使用此功能的成员必须改为使用   ///将其分配给局部变量,并将其用于代码的其余部分   ///这有效地将结构中的一个字段复制到局部变量,以便实现   ///它与其他线程隔离。

简而言之,如果正在执行get方法的同时,其他线程可能正在对结构的字段进行更改或更改结构(例如,通过重新分配此结构类型的类成员字段)因此可能会导致不良副作用,因此get方法必须首先在构造之前制作结构的(本地)副本。

更新:请同时阅读supercats answer,其中详细说明了必须满足哪些条件,以便像制作结构的本地副本(即var self = this;)这样的操作是线程安全的,并且如果不满足这些条件会发生什么。

答案 1 :(得分:8)

如果底层存储位置是可变的,则.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()值报告其构造函数调用所写的时间和其他线程的构造函数调用所报告的时间,而不是两次报告相同的字符串。对于其目的是验证结构字段然后使用它的方法,在验证和使用之间进行字段更改将导致该方法使用未经验证的字段。复制结构字段的内容(通过仅复制字段或复制整个结构)可以避免这种危险。

请注意,对于包含Int64UInt64Double类型字段或包含多个字段的字段的结构,可能会出现{{1}这样的语句这种情况发生在一个线程中,而另一个线程覆盖了存储var temp=this;的位置,最终可能会复制一个包含新旧内容的任意混合的结构。仅当结构包含引用类型的单个字段或32位或更小的基元的单个字段时,才能保证与写入同时发生的读取将产生结构实际保持的某个值,甚至可能有一些怪癖(例如,至少在VB.NET中,this之类的语句可能在调用构造函数之前清除someField = New foo("george")