英文:
PropertyChangedEventArgs pooling
问题
代码部分不要翻译,只返回翻译好的部分:
Has anyone researched if there is a benefit to doing a pool of PropertyChangedEventArgs
objects?
有人研究过是否有必要创建 PropertyChangedEventArgs
对象池吗?
(For those who are not in the subject, I will explain - the PropertyChangedEventArgs
object is part of the INotifyPropertyChanged
interface of the MVVM pattern
)
对于那些不了解的人,我来解释一下 - PropertyChangedEventArgs
对象是 MVVM 模式
中的 INotifyPropertyChanged
接口的一部分。
For simple example:
举个简单的例子:
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
var arg = _pool
.GetOrAdd(propertyName, name => new PropertyChangedEventArgs(name));
PropertyChanged?.Invoke(this, arg);
}
private readonly static ConcurrentDictionary<string, PropertyChangedEventArgs> _pool
= new();
}
我想要减轻 `GC` 的负担,但同时,`String.GetHashCode()` 方法没有被缓存,每次都需要计算,这会增加 CPU 的负载。
What do you think about this question, ladies and gentlemen?
各位女士和先生们,你们对这个问题有什么看法?
<details>
<summary>英文:</summary>
Has anyone researched if there is a benefit to doing a pool of `PropertyChangedEventArgs` objects?
(For those who are not in the subject, I will explain - the `PropertyChangedEventArgs` object is part of the `INotifyPropertyChanged` interface of the `MVVM pattern`)
For simple example:
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
var arg = _pool
.GetOrAdd(propertyName, name => new PropertyChangedEventArgs(name));
PropertyChanged?.Invoke(this, arg);
}
private readonly static ConcurrentDictionary<string, PropertyChangedEventArgs> _pool
= new();
}
I would like to reduce the load on the `GC`, but at the same time, the `String.GetHashCode()` method is not cached and is calculated every time, which increases the load on the CPU.
What do you think about this question, ladies and gentlemen?
</details>
# 答案1
**得分**: 1
以下是翻译好的部分:
"I can't say I've done research here but I have lots of experience with INPC including in contexts outside of XAML. Specifically for my web application I use a homegrown MVVM framework that combines React with C#/WASM via my own binding framework. Performance, especially with INPCs, is thus crucial for me."
"你谈到了创建新的`PropertyChangedEventArgs`实例与在`ConcurrentDictionary`中缓存参数的利弊。"
"Creating new args requires, for each property change - Allocating a small amount of memory on the global heap and later GC'ing it. Remember that .NET caches all hard-coded strings anyway (which your property names would be) so you're not actually allocating extra heap space for the string itself each time, just for the args instance and the pointer to the string. These are all very cheap operations."
"Your approach requires, for each property change - Computing the hash for the property name string - Locking the thread (as you're using a `ConcurrentDictionary`) - cheap but not free - An O(logN) dictionary operation to either find the existing args or add a new instance to the dict. Obviously as you add more and more properties that have change notifications this is going to not be negligible."
"Again without running performance benchmarks, my experience tells me that approach #2 is going to be quite significantly more of a CPU burden than approach #1."
"But if you do the performance benchmarks I hope you post the results, would be very curious."
"Also though it's not a direct answer, if you really wanted to squeeze every possible last bit of performance out of this, you could create a static `PropertyChangedEventArgs` field for each property in each viewmodel class and simply raise `PropertyChanged` in the setter using that static instance. More lines of code, but it would accomplish your goal. I'd be surprised if it resulted in a noticeable performance improvement though."
"Finally here's a 'cute' option - keep a cache of args but as a simple growable list, keyed to the line number of the code. (Note this won't work with `partial` classes!!)"
"private static GrowableList<PropertyChangedEventArgs> _argCache; private void OnPropertyChanged([CallerMemberName] string property = null, [CallerLineNumber] int key = 0) { var args = _argCache[key] ?? (_argCache[key] = new PropertyChangedEventArgs(property)); this.PropertyChanged?.Invoke(this, args); }"
"GrowableList would just be a list-type object that automatically grew to the given index and inserted null values as needed."
"Of course it's wasteful of RAM, but only once per class, but as it would just use array indexing it would be nearly as fast as individual static fields."
"You could also combine the approaches and key a dictionary with the code line number, which would also be a little faster than using the property name."
<details>
<summary>英文:</summary>
I can't say I've done research here but I have lots of experience with INPC including in contexts outside of XAML. Specifically for my web application I use a homegrown MVVM framework that combines React with C#/WASM via my own binding framework. Performance, especially with INPCs, is thus crucial for me.
You're talking here about the pros and cons of creating a new `PropertyChangedEventArgs` instance for every property change notification, versus caching the args in a `ConcurrentDictionary`.
Creating new args requires, for each property change -
- Allocating a small amount of memory on the global heap and later GC'ing it. Remember that .NET caches all hard-coded strings anyway (which your property names would be) so you're not actually allocating extra heap space for the string itself each time, just for the args instance and the pointer to the string. These are all very cheap operations.
Your approach requires, for each property change -
- Computing the hash for the property name string
- Locking the thread (as you're using a `ConcurrentDictionary`) - cheap but not free
- An O(logN) dictionary operation to either find the existing args or add a new instance to the dict. Obviously as you add more and more properties that have change notifications this is going to not be negligible.
Again without running performance benchmarks, my experience tells me that approach #2 is going to be quite significantly more of a CPU burden than approach #1.
But if you do the performance benchmarks I hope you post the results, would be very curious.
----------
Also though it's not a direct answer, if you really wanted to squeeze every possible last bit of performance out of this, you could create a static `PropertyChangedEventArgs` field for each property in each viewmodel class and simply raise `PropertyChanged` in the setter using that static instance. More lines of code, but it would accomplish your goal. I'd be surprised if it resulted in a noticeable performance improvement though.
----------
Finally here's a "cute" option - keep a cache of args but as a simple growable list, keyed to the line number of the code. (Note this won't work with `partial` classes!!)
private static GrowableList<PropertyChangedEventArgs> _argCache;
private void OnPropertyChanged(
[CallerMemberName]
string property = null,
[CallerLineNumber]
int key = 0)
{
var args = _argCache[key] ?? (_argCache[key] = new PropertyChangedEventArgs(property));
this.PropertyChanged?.Invoke(this, args);
}
`GrowableList` would just be a list-type object that automatically grew to the given index and inserted `null` values as needed.
Of course it's wasteful of RAM, but only once per class, but as it would just use array indexing it would be nearly as fast as individual static fields.
You could also combine the approaches and key a dictionary with the code line number, which would also be a little faster than using the property name.
</details>
# 答案2
**得分**: 1
以下是翻译的内容:
IMO在这里更有用的做法是*如果没有人在监听,则不执行任何操作*:
``` c#
var handler = PropertyChanged;
if (handler is not null)
{
var arg = // TODO: your choice of implementation here
handler.Invoke(this, arg);
}
这也可以简写为:
PropertyChanged?.Invoke(this, /* TODO,内联初始化 */);
英文:
IMO the more useful thing to do here is not do anything if no-one is listening:
var handler = PropertyChanged;
if (handler is not null)
{
var arg = // TODO: your choice of implementation here
handler.Invoke(this, arg);
}
This can also be shortened:
PropertyChanged?.Invoke(this, /* TODO, initialize inline */);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论