Any way to avoid Property inline optimization in C

2020-06-17 04:52发布

问题:

So I have a PropertyBag class that is intended to implement INotifyPropertyChanged. In order to make this code work as cleanly as possible and to avoid user error, I am using the stack to get the property name. See, if the property name doesn't match the actual property exactly, then you will have a failure and I am trying to protect from that.

So, here is an example usage of the class:

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

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

The important code for the base PropertyBag is here:

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);
    }
}

So now that you have seen my code, I can tell you the problem... In some cases under release build, the "Foo" setter in the "MyData" constructor appears to be getting optimized to inline as SetProperty(-1). Unfortunately, this inline optimization fails out my SetProperty method because I am no longer calling it from a property! FAIL. It appears that I cannot rely on the StackTrace in this way.

Can anyone A: Figure out a better way to do this but still avoid passing in "Foo" to GetProperty and SetProperty?
B: Figure out a way to tell the compiler to not optimize in this case?

回答1:

Using the stack here is slow and unnecessary; I would simply use:

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

(hint: I've done a lot of work with custom property models; I know that this works well...)

Another alternative is an object key (use reference equality to compare) - a lot of ComponentModel works this way, as do some of the properties in WF/WPF:

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

Of course, you could declare a type for the keys (with a Name property), and use that:

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

etc; however, to answer the question: mark it (but don't do this) with:

[MethodImpl(MethodImplOptions.NoInlining)]

or

[MethodImpl(MethodImplOptions.NoOptimization)]

or

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



回答2:

Using the stack is not a good idea. You are relying on internal implementation of the compiler to artificially tie in your property bag to the language properties.

  1. having a requirement to add the MethodImpl attribute makes the use of your property bag non-transparent for other developers.
  2. even if the property bag has the MethodImpl attribute, nothing guarantees you it will be the first frame on the call stack. It is possible that the assembly was instrumented or modified to inject calls between the actual property and the call to your property bag. (Think aspect programming)
  3. New languages or even a future version of the C# compiler might decorate the property accessors in a different way then '_get' and '_set'
  4. Constructing the call stack is relatively slow operation, as it requires the internal compressed stack to be decompressed and the name of each type and method to be obtained using reflection.

You should really just implement your property bag accessors to take a parameter to identify the property - either a string name (like Hastable) or an object (like the WPF dependency property bag)



回答3:

Try the new [CallerMemberName] attribute.

Place it on a parameter to your method ([CallerMemberName] callerName = null) and the compiler will rewrite all calls to your method to pass the caller name automatically (your calls don't pass the parameter at all).

It doesn't eliminate any optimizations, and is much faster than lambdas or reflection or stacks, and works in Release mode.

P.S. if CallerMemberNameAttribute doesn't exist in your version of the framework, just define it (empty). It's a language feature, not a framework feature. When the compiler sees [CallerMemberNameAttribute] on a parameter, it just works.



回答4:

You can try to create a T4 template file so your properties could be automatically generated with correct property names for GetProperty() and SetProperty() methods.

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



回答5:

If you wish to avoid hard coded strings you can use:

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));
}

Add more sanity checking as you see fit

then the property gets become

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

sets change in the same way.

The GetCurrentMethod() also traverses the stack but does so via an (internal) unmanaged call relying on stack markers so will work in release mode as well.

Alternatively for a quick fix [MethodImpl] with MethodImplAttributes.NoOptimization) or MethodImplAttributes.NoInlining will also work albeit with a performance hit (though given that you are traversing the stack frame each time this hit is negligible).

A further technique, getting some level of compile time checking is:

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); }     
}

where Xxxxx is the class in which the property is defined. If the static nature causes issues (threading or otherwise making it an instance value is also possible).

I should point out that these techniques are really for interests sake, I am not suggesting they are good general techniques to use.