Skip to content

key: avoid allocation for non Rc<str> From implementations#3855

Closed
flumm wants to merge 1 commit into
yewstack:masterfrom
flumm:key-rc-string
Closed

key: avoid allocation for non Rc<str> From implementations#3855
flumm wants to merge 1 commit into
yewstack:masterfrom
flumm:key-rc-string

Conversation

@flumm
Copy link
Copy Markdown
Contributor

@flumm flumm commented May 8, 2025

From<&str> for Rc clones the content of the &str, so there are two allocations for most types: to_string() and Rc::from.

Using a Rc instead of Rc internally can avoid this additional allocation for most types, and completely avoid it for String. This will now allocate when using it for Rc directly (which wasn't the case previously).

Since direct use of Rc will not happen very often, I'd argue that this tradeoff is better than the status quo.

Description

change the internal type of Key to avoid allocations in most cases

Checklist

  • I have reviewed my own code
  • I have added tests

`From<&str> for Rc` clones the content of the &str, so there are two
allocations for most types: `to_string()` and `Rc::from`.

Using a Rc<String> instead of Rc<str> internally can avoid this
additional allocation for most types, and completely avoid it for
`String`. This will now allocate when using it for Rc<str> directly
(which wasn't the case previously).

Since direct use of Rc<str> will not happen very often, I'd argue that
this tradeoff is better than the status quo.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2025

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)

✅ None of the examples has changed their size significantly.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2025

Visit the preview URL for this PR (updated for commit 18ad8a9):

https://yew-rs-api--pr3855-key-rc-string-s78hgdv5.web.app

(expires Thu, 15 May 2025 11:04:03 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2025

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.121 ns      │ 2.269 ns      │ 2.124 ns      │ 2.128 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.09 ns       │ 2.243 ns      │ 2.095 ns      │ 2.102 ns      │ 100     │ 1000000000

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2025

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 290.925 301.327 292.533 3.223
Hello World 10 495.524 565.730 511.026 26.621
Function Router 10 1614.300 1639.557 1625.308 6.564
Concurrent Task 10 1006.118 1007.228 1006.724 0.404
Many Providers 10 1100.195 1139.115 1113.690 10.973

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 290.922 297.366 292.394 2.497
Hello World 10 487.209 523.359 494.311 11.111
Function Router 10 1587.839 1640.816 1605.720 18.219
Concurrent Task 10 1005.373 1006.764 1006.302 0.483
Many Providers 10 1091.159 1128.924 1107.344 14.196

@WorldSEnder
Copy link
Copy Markdown
Member

WorldSEnder commented May 8, 2025

I fail to see how this helps in its current state.

From<&str> for Rc clones the content of the &str, so there are two allocations for most types: to_string() and Rc::from.

This is not true, only one allocation is done. The existing code uses the Rc<str>: From<str> impl for the conversion which only uses one allocation - the one for the Rc. The current allocated layout for a key thus looks like this:

Rc<str> ----> +--- *Rc<str> --------+
              | strong: Cell<usize> |
              | weak: Cell<usize>   |
              | value: str          |
              +---------------------+

Note that this is a dynamically sized value, and the single allocation of the Rc can not be avoided by changing the pointed-to type to String, since the strong and weak counts have to be stored in the shared memory regardless.

Using a Rc<String> instead of Rc<str> internally can avoid this additional allocation for most types, and completely avoid it for String

Rc<String> ----> +--- *Rc<String> -----+
                 | strong: Cell<usize> |
                 | weak: Cell<usize>   |
                 | value: String ------|------> +---- *String ------+
                 +---------------------+        | bytes: str        |

It can not avoid the allocation in the Rc as outlined above and it introduces a second indirection. What it does arguably avoid is a memcopy of the string. Since most of these strings should be either static str or small strings, I don't think this extra indirection (which needs to be followed in every check) is worth the avoided memcopy of a couple of bytes though. Feel free to prove this wrong with benchmarks. In any case,

This will now allocate when using it for Rc directly (which wasn't the case previously).

I fail to see that Rc<String> is more common than Rc<str>. If anything, Rc<str> is more common and should be the cheaper of the two.

Since direct use of Rc will not happen very often

I agree. Yet, &'static str are often used and now incur two allocations and a memcopy compared to one allocation and one memcopy as before.


There is one part that might be worth to improve. The current key_impl_from_to_string do incur a very temporary small allocation that is immediately copied into the longer lived allocation in the Rc.
I really doubt that this will save a lot though, modern allocators should be good at handling small short lived allocations like this. Might be worth to not have either way, if there is a good safe way to do so.

@flumm
Copy link
Copy Markdown
Contributor Author

flumm commented May 8, 2025

I fail to see how this helps in its current state.

From<&str> for Rc clones the content of the &str, so there are two allocations for most types: to_string() and Rc::from.

This is not true, only one allocation is done. The existing code uses the Rc<str>: From<str> impl for the conversion which only uses one allocation - the one for the Rc. The current allocated layout for a key thus looks like this:

Rc<str> ----> +--- *Rc<str> --------+
              | strong: Cell<usize> |
              | weak: Cell<usize>   |
              | value: str          |
              +---------------------+

i think allocation was the wrong choice of words here, I of course meant memcopy.

  • the Rc<str> case did no memcopy (just a new allocation of Rc)
  • &str did one allocation + memcopy call
  • all other types did an allocation + either an addition alloc + memcopy or 2 memcopy calls

the last part is what i wanted to avoid, but see below, the double indirection is probably not worth it

Note that this is a dynamically sized value, and the single allocation of the Rc can not be avoided by changing the pointed-to type to String, since the strong and weak counts have to be stored in the shared memory regardless.

Using a Rc<String> instead of Rc<str> internally can avoid this additional allocation for most types, and completely avoid it for String

Rc<String> ----> +--- *Rc<String> -----+
                 | strong: Cell<usize> |
                 | weak: Cell<usize>   |
                 | value: String ------|------> +---- *String ------+
                 +---------------------+        | bytes: str        |

It can not avoid the allocation in the Rc as outlined above and it introduces a second indirection. What it does arguably avoid is a memcopy of the string. Since most of these strings should be either static str or small strings, I don't think this extra indirection (which needs to be followed in every check) is worth the avoided memcopy of a couple of bytes though. Feel free to prove this wrong with benchmarks. In any case,

yes, just noticed the double indirection too, so that's too bad. I don't have a good benchmark for this sadly.. but yes, if comparing is more often done than allocating, it's not worth that

This will now allocate when using it for Rc directly (which wasn't the case previously).

I fail to see that Rc<String> is more common than Rc<str>. If anything, Rc<str> is more common and should be the cheaper of the two.

Since direct use of Rc will not happen very often

I agree. Yet, &'static str are often used and now incur two allocations and a memcopy compared to one allocation and one memcopy as before.

There is one part that might be worth to improve. The current key_impl_from_to_string do incur a very temporary small allocation that is immediately copied into the longer lived allocation in the Rc. I really doubt that this will save a lot though, modern allocators should be good at handling small short lived allocations like this. Might be worth to not have either way, if there is a good safe way to do so.

yeah this was exactly what i was trying to avoid, but I did not find a good way to do that besides changing
the internal type.

the only trivial improvement that i can see is to use a manual implementation for String that does not call to_string() but consumes the value instead directly

@WorldSEnder
Copy link
Copy Markdown
Member

WorldSEnder commented May 8, 2025

the only trivial improvement that i can see is to use a manual implementation for String that does not call to_string() but consumes the value instead directly

That is indeed a good catch! I haven't even noticed that we effectively clone the string there. Changing the impl to a manual

impl From<String> for Key {
    fn from(key: String) -> Self {
        Self::from(key.as_str())
    }
}

should do the trick, I suppose.

EDIT: Reading the source code, the ToString impls of basically all some primitives (and String) do get specialized to something more optimal than going through the Display machinery, so at least that should be reasonably efficient already.

@flumm
Copy link
Copy Markdown
Contributor Author

flumm commented May 9, 2025

ok, i'd close this pull request and do another one for the string case, or should i force push here ?

@WorldSEnder
Copy link
Copy Markdown
Member

Yes please do so

@WorldSEnder WorldSEnder added won't fix performance A-yew Area: The main yew crate labels May 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-yew Area: The main yew crate performance won't fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants