-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: ConditionalWeakTable<TKey,TValue>.GetOrAdd #89002
Comments
Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices Issue DetailsBackground and motivationCurrently, there doesn't seem to be a way to implement a lock-free caching algorithm using a // Try to get an existing value for the key
if (table.TryGetValue(key, out Foo? result))
{
return result;
}
// Value does not exist, create it (this is potentially expensive)
result = CreateValueForKey(key);
// Add the value into the table in a way that makes it so that if we're racing against
// another thread, we're guaranteeing that all threads will still only see and retrieve
// the same instance from the table.
table.GetValue(key, _ => result); That last step would ideally be some Looking at public void GetOrAdd(TKey key, TValue value)
{
if (key is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
lock (_lock)
{
int entryIndex = _container.FindEntry(key, out object? existingValue);
if (entryIndex != -1)
{
return Unsafe.As<TValue>(existingValue);
}
CreateEntry(key, value);
}
} Essentially just a small variation compared to the existing API Proposalnamespace System.Runtime.CompilerServices;
public sealed class ConditionalWeakTable<TKey, TValue>
{
+ public void GetOrAdd(TKey key, TValue value);
} API UsageUpdating the example above to use the new API: if (table.TryGetValue(key, out Foo? result))
{
return result;
}
result = CreateValueForKey(key);
return table.GetOrAdd(key, result); Alternative DesignsKeep using RisksNone that I can see, it's just a small new API with no additional changes needed.
|
Another approach for allowing closure-less use of namespace System.Runtime.CompilerServices;
public sealed class ConditionalWeakTable<TKey, TValue>
{
+ public delegate TValue CreateValueWithStateCallback<TState>(TKey key, TState state);
+ public TValue GetValue<TState>(TKey key, CreateValueWithStateCallback<TState> createValueCallback, TState state);
} It's very similar to the existing I think this would work for your use case and others. For example, in NativeAOT ComWrappers the |
Honestly, both methods seem useful to me, in different scenarios. For instance:
Updated proposal: namespace System.Runtime.CompilerServices;
public sealed class ConditionalWeakTable<TKey, TValue>
{
+ public TValue GetOrAdd(TKey key, TValue value);
+ public TValue GetValue<TState>(TKey key, CreateValueCallback<TState> createValueCallback, TState state);
+ public delegate TValue CreateValueCallback<TState>(TKey key, TState state)
+ where TState : allows ref struct
} @tannergooding @eiriktsarpalis are there any other concerns with the proposed shape? If not, could you help mark this as ready to review, so we can move this forward? I'd be happy to contribute these two if they get approved. We can use them to improve |
In the original example the only thing that your closure is capturing is the key, which could have been passed parametrically. Would it be possible to update the proposal in the OP, and show an example that better conveys what the new Apart from that the proposal looks good to me, although I'm not sure if the original method should be called |
I've updated the OP to include the two proposed APIs and use examples of both 🙂
The main reason why I named it |
I guess I was trying to say that the example could be written like so: table.GetValue(key, key => CreateValueForKey(key, configuration)); I initially misinterpreted this as it being an issue of the delegate not accepting the key, but this should make it more clear that the actual problem is the delegate needing to capture table.GetValue(key, static (key, configuration) => CreateValueForKey(key, configuration), configuration); |
I've updated the OP to also include that example and mention that it'd waste a display class + delegate allocation 👍 To clarify, the proposal is intentionally including both APIs, to give users more flexibility depending on the scenario. |
namespace System.Runtime.CompilerServices;
public sealed partial class ConditionalWeakTable<TKey, TValue>
{
+ public TValue GetOrAdd(TKey key, TValue value);
+ public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);
+ public TValue GetOrAdd<TArg>(TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
+ where TArg : allows ref struct;
+ [EditorBrowsable(EditorBrowsableState.Never)]
public TValue GetValue(TKey key, CreateValueCallback createValueCallback);
+ [EditorBrowsable(EditorBrowsableState.Never)]
public TValue GetOrCreateValue(TKey key);
+ [EditorBrowsable(EditorBrowsableState.Never)]
public delegate TValue CreateValueCallback(TKey key);
} |
@Sergio0694 would you be interested in contributing the implementation? |
Yesss, happy to take this one! 😄 |
@jkotas I have a follow up question on public sealed partial class ConditionalWeakTable<TKey, TValue>
{
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(TValue))]
[EditorBrowsable(EditorBrowsableState.Never)]
public TValue GetOrCreateValue(TKey key);
} Would this be sufficient and allow us to drop |
I do not understand nuances of the reflection annotations well enough to tell whether this would be sufficient. If you try to do this, do you see any build breaks or complains from the analyzers? |
Trying this out, I realized that you can't currently use a type parameter for a type argument in an attribute. I'm out of ideas on how to fix this then 🥲 |
Would adding |
It would have to be tracked as a breaking change. I am not sure whether we have a prior art for breaking changes of this nature. |
It would be very difficult to add support for "keep members on the T, but only if a specific instance method is called" (and especially so if TValue is a class due to generic sharing; the compiler considers the instance method called on any canonically equivalent T). I don't think there's anything that can be done. |
@MichalStrehovsky what do you think about the breaking change if removing the annotation and marking the method as trim unsafe? Is that something you think we could try to do? People calling it would still get the proper warning, which they can easily fix by either using dynamic dependency, or better yet, just calling the new APIs, which are also faster anyway. |
Are we purely discussing improved size or are there other considerations? If it's size only, it would have to be really significant to even consider. "We broke your code but look it can be 0.8% smaller" is a much harder sell than "We broke your code but look it can be 8% smaller". Also note the existing annotation is to keep the public parameterless constructor only - if there is no public parameterless constructor on the T, the size impact should be zero even with the annotation. |
Background and motivation
Currently, there doesn't seem to be a way to implement a lock-free caching algorithm using a
ConditionalWeakTable<TKey, TValue>
without having to allocate a delegate + closure to insert a value into the table. That is, consider this:That last step would ideally be some
table.GetOrAdd(key, result)
call, with no delegate needed.I know you can also do
TryAdd
andGetValue
again iffalse
, but that does one extra unnecessary lookup.Also consider this similar example:
This works, but it captures
configuration
and allocates a display and non-cached delegate unnecessarily.Looking at
ConditionalWeakTable<TKey, TValue>
, it seems easy enough to add these new APIs:These two convenience methods can be used where appropriate. For instance, it might be simpler in some cases to use the new
GetValue
overload, whereas in other cases where you might want more control, you might manually try the first lookup, then do some additional work if that fails, and finally callGetOrAdd
with the new value you produced.API Proposal
API Usage
Updating the example above to use the new API:
AND
Alternative Designs
Keep using
GetValue
and waste the delegate + closure allocation (also it's much clunkier).Risks
None that I can see, it's just two small new APIs with no additional changes needed.
The functionality is the same as already available today, just more effcient.
The text was updated successfully, but these errors were encountered: