【问题标题】:Any way to avoid Property inline optimization in C#?有什么方法可以避免 C# 中的属性内联优化?
【发布时间】:2009-02-17 16:32:38
【问题描述】:

所以我有一个旨在实现 INotifyPropertyChanged 的​​ PropertyBag 类。为了使此代码尽可能干净地工作并避免用户错误,我使用堆栈来获取属性名称。看,如果属性名称与实际属性不完全匹配,那么您将失败,我正在努力避免这种情况。

所以,这里是一个类的用法示例:

public class MyData : PropertyBag
{
    public MyData()
    {
        Foo = -1;
    }

    public int Foo
    {
        get { return GetProperty<int>(); }
        set { SetProperty(value); }
    }
}

基础 PropertyBag 的重要代码在这里:

public abstract class PropertyBag : INotifyPropertyChanged
{
    protected T GetProperty<T>()
    {
        string propertyName = PropertyName((new StackTrace()).GetFrame(1));
        if (propertyName == null)
            throw new ArgumentException("GetProperty must be called from a property");

        return GetValue<T>(propertyName);
    }

    protected void SetProperty<T>(T value)
    {
        string propertyName = PropertyName((new StackTrace()).GetFrame(1));
        if (propertyName == null)
            throw new ArgumentException("SetProperty must be called from a property");

        SetValue(propertyName, value);
    }

    private static string PropertyName(StackFrame frame)
    {
        if (frame == null) return null;
        if (!frame.GetMethod().Name.StartsWith("get_") &&
           !frame.GetMethod().Name.StartsWith("set_"))
            return null;

        return frame.GetMethod().Name.Substring(4);
    }
}

既然您已经看到了我的代码,我可以告诉您问题...在某些情况下,在发布版本下,“MyData”构造函数中的“Foo”设置器似乎被优化为内联为 SetProperty(- 1)。不幸的是,这种内联优化使我的 SetProperty 方法失败,因为我不再从属性中调用它!失败。看来我不能以这种方式依赖 StackTrace。

任何人都可以 A:想出一个更好的方法来做到这一点,但仍然避免将“Foo”传递给 GetProperty 和 SetProperty?
B:想办法告诉编译器在这种情况下不要优化?

【问题讨论】:

  • 我忘了说 - 不要使用字典来存储 - 因为您往往拥有少量属性,所以扫描平面列表实际上要快得多(分水岭大约有 120 个键)。
  • 你可以把它们变成虚拟的......

标签: c# stack-trace


【解决方案1】:

在这里使用堆栈是缓慢且不必要的;我会简单地使用:

get { return GetProperty<int>("Foo"); }
set { SetProperty("Foo", value); }

(提示:我在自定义属性模型方面做了很多工作;我知道这很好用……)

另一种选择是对象键(使用引用相等性进行比较) - 很多 ComponentModel 以这种方式工作,WF/WPF 中的一些属性也是如此:

static readonly object FooKey = new object();
...
get { return GetProperty<int>(FooKey); }
set { SetProperty(FooKey, value); }

当然,您可以为键声明一个类型(使用 Name 属性),然后使用它:

static readonly PropertyKey FooKey = new PropertyKey("Foo");

等;但是,要回答这个问题:标记它(但不要这样做):

[MethodImpl(MethodImplOptions.NoInlining)]

[MethodImpl(MethodImplOptions.NoOptimization)]

[MethodImpl(MethodImplAttributes.NoOptimization
    | MethodImplAttributes.NoInlining)]

【讨论】:

  • 感谢您的回复。那个 NoInlining 属性是我想知道的......但我同意......我不会使用它。它要求用户知道这样做,这比使用“Foo”更糟糕。令人敬畏的反应有很多原因。谢谢。
  • 为了清楚起见,我使用这种机制是为了避免将来出现重构问题。如果使用重构工具更改属性的名称,那么代码会突然失败,因为名称需要与属性相同。
  • 更新:在 C#6 中,您可以使用 'nameof(...)' 方法来获取属性的名称。这可以防止您将来重构问题。
【解决方案2】:

使用堆栈不是一个好主意。您依赖编译器的内部实现来人为地将您的属性包与语言属性绑定。

  1. 要求添加 MethodImpl 属性会使其他开发人员无法使用您的属性包。
  2. 即使属性包具有MethodImpl 属性,也无法保证它将成为调用堆栈上的第一帧。程序集可能被检测或修改以在实际属性和对您的属性包的调用之间注入调用。 (思考方面编程)
  3. 新语言甚至未来版本的 C# 编译器可能会以不同于 '_get''_set' 的方式装饰属性访问器
  4. 构造调用栈是一个比较慢的操作,因为它需要对内部压缩栈进行解压,并通过反射获取每个类型和方法的名称。

你真的应该只实现你的属性包访问器来获取一个参数来识别属性 - 一个字符串名称(如 Hastable)或一个对象(如 WPF 依赖项属性包)

【讨论】:

    【解决方案3】:

    试试新的 [CallerMemberName] 属性。

    将它放在您的方法的参数上 ([CallerMemberName] callerName = null),编译器将重写对您的方法的所有调用以自动传递调用者名称(您的调用根本不传递参数)。

    它不会消除任何优化,并且比 lambdas 或反射或堆栈快得多,并且可以在 Release 模式下工作。

    附:如果 CallerMemberNameAttribute 在您的框架版本中不存在,只需定义它(空)。这是一个语言特性,而不是一个框架特性。当编译器在参数上看到 [CallerMemberNameAttribute] 时,它就可以工作了。

    【讨论】:

    • CallerMemeberName 很棒,但请注意它需要 C# 5.0 编译器(即使您自己定义它[例如,以 .NET Framework 4.0 为目标],但您使用的是 Visual Studio例如 2010 年,它将无法工作——您需要一个了解此属性的编译器)。
    【解决方案4】:

    您可以尝试创建一个 T4 模板文件,以便使用正确的属性名称自动生成您的属性,用于 GetProperty() 和 SetProperty() 方法。

    T4: Text Template Transformation Toolkit T4 (Text Template Transformation Toolkit) Code Generation - Best Kept Visual Studio Secret

    【讨论】:

      【解决方案5】:

      如果您希望避免使用硬编码字符串,您可以使用:

      protected T GetProperty<T>(MethodBase getMethod)
      {
          if (!getMethod.Name.StartsWith("get_")
          {
              throw new ArgumentException(
                  "GetProperty must be called from a property");
          }
          return GetValue<T>(getMethod.Name.Substring(4));
      }
      

      添加更多您认为合适的健全性检查

      然后属性就变成了

      public int Foo
      {
          get { return GetProperty<int>(MethodInfo.GetCurrentMethod()); }     
      }
      

      集合以同样的方式变化。

      GetCurrentMethod() 也遍历堆栈,但通过依赖堆栈标记的(内部)非托管调用执行此操作,因此也可以在释放模式下工作。

      使用 MethodImplAttributes.NoOptimization) 或 MethodImplAttributes.NoInlining 快速修复 [MethodImpl] 或者 MethodImplAttributes.NoInlining 也可以使用,尽管性能受到影响(尽管每次该影响可以忽略不计时您都在遍历堆栈帧)。

      获得某种程度的编译时检查的进一步技术是:

      public class PropertyHelper<T>
      {
          public PropertyInfo GetPropertyValue<TValue>(
              Expression<Func<T, TValue>> selector)
          {
              Expression body = selector;
              if (body is LambdaExpression)
              {
                  body = ((LambdaExpression)body).Body;
              }
              switch (body.NodeType)
              {
                  case ExpressionType.MemberAccess:
                      return GetValue<TValue>(
                          ((PropertyInfo)((MemberExpression)body).Member).Name);
                  default:
                      throw new InvalidOperationException();
              }
          }
      }
      
      private static readonly PropertyHelper<Xxxx> propertyHelper 
          = new PropertyHelper<Xxxx>();
      
      public int Foo
      {
          get { return propertyHelper.GetPropertyValue(x => x.Foo); }     
      }
      

      其中 Xxxxx 是定义属性的类。如果静态特性导致问题(线程化或以其他方式使其成为实例值也是可能的)。

      我应该指出,这些技术确实是为了利益,我并不是说它们是很好的通用技术。

      【讨论】:

      • 我喜欢这个。它使得重构代码不会导致失败。
      • 不适用于 setter 属性——(它们以“set_”开头)——这也是 OP 已经在做的事情——但是如果你有优化,JITer 会内联你的方法打开 - 所以这完全忽略了 OP 的问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多