Add get_value_ref_or_add_default() to HashMap#113448
Conversation
co-authored-by: Ryan-000 <73148864+Ryan-000@users.noreply.github.com>
There was a problem hiding this comment.
Does adding this method make a big difference in practice for where this is used in the follow-up PRs? I'd be interested to see performance tests for this.
The alternative, as usual, is just using the getptr API, followed by an insertion if necessary. While slower, for most use-cases it suffices.
|
This avoids hashing and looking up the same key twice by providing a single lookup-or-insert operation. This PR would likely be OK with the usual slower I’m basing this on C#’s CollectionsMarshal.GetValueRefOrAddDefault pattern which I use a lot; and I’ve noticed several places in Godot (e.g. Eventually, when I have the time, I'll start submitting patches for them. Lines 3035 to 3043 in 2ecefad |
No change is truly free. In this case, the change makes If this change should be merged, we need a better reason for it than "might as well do it" — we need a proof that it positively affects bottlenecks. Please refer to our optimization guidelines for the expectations of optimization PRs. |
|
If the performance of There are many call sites that perform repeated hashing and lookups, which can be reduced. Here is an example from godot/scene/animation/animation_mixer.cpp Line 159 in 8105ff7 We currently use has() and then operator[], which involves multiple lookups on both paths.
StringName key = /* some key */
if (!animation_set.has(key)) { // 1) Hash key and lookup.
AnimationData ad;
ad.animation = K.value;
ad.animation_library = lib.name;
ad.name = key;
ad.last_update = animation_set_update_pass;
animation_set.insert(ad.name, ad); // 2) Hash key and lookup again.
cache_valid = false;
} else {
AnimationData &ad = animation_set[key]; // 2) Hash key and lookup again.
if (ad.last_update != animation_set_update_pass) {
if (ad.animation != K.value || ad.animation_library != lib.name) {
clear_cache_needed = true;
ad.animation = K.value;
ad.animation_library = lib.name;
}
ad.last_update = animation_set_update_pass;
}
}Faster StringName key = /* some key */
AnimationData *ad = animation_set.getptr(key); // 1) Hash key and lookup
if (!ad) {
ad = &animation_set.insert(key, AnimationData())->value; // 2) Hash key and lookup again.
ad->animation = K.value;
ad->animation_library = lib.name;
ad->name = key;
ad->last_update = animation_set_update_pass;
cache_valid = false;
} else {
if (ad->last_update != animation_set_update_pass) {
if (ad->animation != K.value || ad->animation_library != lib.name) {
clear_cache_needed = true;
ad->animation = K.value;
ad->animation_library = lib.name;
}
ad->last_update = animation_set_update_pass;
}
}Fastest, hash key once, lookup once. StringName key = /* some key */
bool was_added = false;
AnimationData &ad = animation_set.get_value_ref_or_add_default(key, was_added); // Only 1 hash key and lookup.
if (was_added) {
ad.animation = K.value;
ad.animation_library = lib.name;
ad.name = key;
ad.last_update = animation_set_update_pass;
cache_valid = false;
} else {
if (ad.last_update != animation_set_update_pass) {
if (ad.animation != K.value || ad.animation_library != lib.name) {
clear_cache_needed = true;
ad.animation = K.value;
ad.animation_library = lib.name;
}
ad.last_update = animation_set_update_pass;
}
}The tradeoff is one additional method on More broadly, much of the animation optimization work involved reducing unnecessary lookups, which is why I want to make this pattern easy to use going forward. Otherwise, I would have just used the |
That would introduce additional complexity to the class, and increase the chance of code cache misses.
Sure, I understand the trade-off on a theoretical level. For example, insertion is quite rare (usually just during setup). So even if the |
|
Furthermore, if this is truly effective, I believe it should be applied in areas beyond just Animation. |
co-authored-by: @Ryan-000