As you can remember, we have a complete solution for persistent variables, but it is painfully slow. And that's because we use StackFrame class which is not designed to be used in normal code, so its performance is rather low. On the other hand, StackFrame would provide more information than just the Native Offset we're reading from, and to elaborate this other information, it costs time which we could spare. So we could use our own implementation of a light-weighted StackFrame class. Its code is visible with every cheap IL de-compiler. Yes, you could, but the content is complex and if something changes on the IL framework, the StackFrame class will be changed accordingly, but will your implementation changed accordingly as well? Probably not. Rather your application will compile but chaos rising at runtime ...
So let's see what other possibility we have. We could use these pre-compiler driven attributes called CallerFilePath, CallerMemberName, CallerLineNumber. Using these three attributes, our code will work as fast as any simple code (in comparison, the use of StackFrame costs about 20 times more time to run!).
So when we change our Persistent class to that technique it would look like this:
public static class Persistent<T> where T : struct { #region StaticValues static readonly Dictionary<string, Dictionary<string, Dictionary<int, T[]>>> staticValues; public static ref T Static(T value, [CallerFilePath] string callerFilePath = default, [CallerMemberName] string callerMemberName = default, [CallerLineNumber] int callerLineNumber = default) { if (!staticValues.ContainsKey(callerFilePath)) staticValues.Add(callerFilePath, new Dictionary<string, Dictionary<int, T[]>>()); if (!staticValues[callerFilePath].ContainsKey(callerMemberName)) staticValues[callerFilePath].Add(callerMemberName, new Dictionary<int, T[]>()); if (!staticValues[callerFilePath][callerMemberName].ContainsKey(callerLineNumber)) staticValues[callerFilePath][callerMemberName].Add(callerLineNumber, new T[] { value });
return ref staticValues[callerFilePath][callerMemberName][callerLineNumber][0]; } #endregion #region NonStaticValues static readonly Dictionary<string, Dictionary<string, Dictionary<int, Dictionary<INotifyDisposed, T[]>>>> nonStaticValues; public static ref T Local(T value, INotifyDisposed callerInstance, [CallerFilePath] string callerFilePath = default, [CallerMemberName] string callerMemberName = default, [CallerLineNumber] int callerLineNumber = default) { if (!nonStaticValues.ContainsKey(callerFilePath)) nonStaticValues.Add(callerFilePath, new Dictionary<string, Dictionary<int, Dictionary<INotifyDisposed, T[]>>>()); if (!nonStaticValues[callerFilePath].ContainsKey(callerMemberName)) nonStaticValues[callerFilePath].Add(callerMemberName, new Dictionary<int, Dictionary<INotifyDisposed, T[]>>()); if (!nonStaticValues[callerFilePath][callerMemberName].ContainsKey(callerLineNumber)) nonStaticValues[callerFilePath][callerMemberName].Add(callerLineNumber, new Dictionary<INotifyDisposed, T[]>()); if (!nonStaticValues[callerFilePath][callerMemberName][callerLineNumber] .ContainsKey(callerInstance)) { callerInstance.Disposed += (sender, e) => { if (nonStaticValues[callerFilePath][callerMemberName][callerLineNumber] .Remove(callerInstance)) { if (nonStaticValues[callerFilePath][callerMemberName][callerLineNumber] .Keys.Count == 0) { nonStaticValues[callerFilePath][callerMemberName] .Remove(callerLineNumber); if (nonStaticValues[callerFilePath][callerMemberName].Keys.Count == 0) { nonStaticValues[callerFilePath].Remove(callerMemberName); if (nonStaticValues[callerFilePath].Remove(callerMemberName)) { if (nonStaticValues[callerFilePath].Keys.Count == 0) { nonStaticValues.Remove(callerFilePath); } } } } } return; }; nonStaticValues[callerFilePath][callerMemberName][callerLineNumber] .Add(callerInstance, new T[] { value }); } return ref nonStaticValues[callerFilePath][callerMemberName][callerLineNumber] [callerInstance][0]; } #endregion static Persistent() { staticValues = new Dictionary<string, Dictionary<string, Dictionary<int, T[]>>>(); nonStaticValues = new Dictionary<string, Dictionary<string, Dictionary<int, Dictionary<INotifyDisposed, T[]>>>>(); } }
First, we see, that the code is quite grown, but this is the smallest drawback of our change. But let's first check if the consumption has changed in any way:
class Test: INotifyDisposed { public void Method1() { ref int myInt = ref Persistent<int>.Local(0, this); myInt++; Console.WriteLine($"myInt for this object instance is now {myInt}"); ref int myIntStatic = ref Persistent<int>.Static(0); myIntStatic++; Console.WriteLine($"myIntStatic object independent is now {myIntStatic}"); } #region INotifyDisposed & IDisposable implementation public event EventHandler Disposed; private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { Disposed?.Invoke(this, new EventArgs()); } disposedValue = true; } } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } #endregion }
No, the code for the consumer is still the same. And all works fine and super fast with our sample Test class. So, where's the problem then?
The answers are called 'Optional parameters' and 'Uniqueness'.
You have sincerely noticed the optional parameters in the methods Local() and Static() on Persistent class. A developer could try to assign values to callerFilePath, callerMemberName and callerLineNumber arguments, either because he don't know how to use it exactly or to thickle out some values which are not supposed to be read and/or modified by his/other scope. So these optional arguments can lead either to chaos in your application or even leaks sensitive data. You wanna agree, this solution begins to smell, right?
But there is even another very bad side effects when using the pre-compiler attributes to help building a unique identifier for memory's key. It is not unique. The callerFilePath value is built when compiling. So imagine your colleague is using our feature as well, he builds a class library and has a code file called tests.cs which will lead in built phase to c:\a\test.cs. And now you are gonna use this library but having also a test.cs which also leads to c:\a\test.cs in the built phase. Vóilà! Your and your collegue's code have now big potential to interfere each other's code when using persistent values in methods. You can state: "That's almost impossible!" Maybe, but one time is the first time and to figure out what's going wrong with your application in such a situation can be very annoying. And besides the callerFilePath, what about the callerLineNumber? Let's imagine you write such a code:
public void Method1() { ref int myIntStatic = ref Persistent<int>.Static(0); ref int myIntStatic2 = ref Persistent<int>.Static(0); myIntStatic++; myIntStatic2++; Console.WriteLine($"hey! myIntSatic ({myIntStatic}) does affect myIntStatic2 {myIntStatic2} and vice versa, " + $"what's going on here?"); }
Yes, the line number is not enough to identify your persistent value. All use of Persistent.Static (or Local) on the same line with the same type will share the same memory. This does not just smells, now it definitely stinks!
Next and final chapter: The conclusion
No comments:
Post a Comment