Skip to content
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

Refactor form layout to not allocate a slice at all #4824

Merged
merged 7 commits into from
May 2, 2024

Conversation

Jacalz
Copy link
Member

@Jacalz Jacalz commented May 1, 2024

Description:

This clearly shows the big benefit of incrementally refactoring code until it is smaller and easier to read.
The issue was already partly resolved with #4822 which successfully managed
to refactor the code and remove a lot of unnecessary parts and thus decrease allocations by 75%. That refactor
made the code easier to reasoned about and I managed to remove the last remaining allocation.

This still needs some cleanup. While allocating less, it has slightly more code duplication and is a but harder to read in places while it is better in others.

Fixes #4821

Checklist:

  • Tests included.
  • Lint and formatter run with no errors.
  • Tests all pass.

@coveralls
Copy link

coveralls commented May 2, 2024

Coverage Status

coverage: 64.98% (-0.02%) from 64.998%
when pulling c66a712 on Jacalz:remove-form-allocs
into 1178a91 on fyne-io:develop.

@dweymouth
Copy link
Contributor

dweymouth commented May 2, 2024

This now requires calling MinSize() 2x per object during Layout, instead of 1x per object before (except in the case of canvas.Text where we still called it a second time). I'm not sure eliminating slice allocations in this way will be worth the tradeoff of 2x MinSize invocation, especially considering they can be user widgets with non-optimized MinSize calculations. Might this be a case where we prefer to use sync.Pool to mitigate the effects of the allocations rather than eliminate them completely?

@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

Indeed. I know. I think the code is a lot easier to read now and the MinSize calculation is a lot faster (still only doing one lookup). It might be worth it, especially if I can manage to get #4681 finished.

@dweymouth
Copy link
Contributor

Will #4681 work for user widgets too? I'd kind of consider that a necessity before thinking this trade off is worth it over using sync.Pool, but maybe it's just me. It's hard to benchmark too because of the wide variety of possible user widgets (and MinSize implementations) there can be.

@dweymouth
Copy link
Contributor

dweymouth commented May 2, 2024

Another approach could be to combine both methods and define a static array in the formLayout struct itself, like [16]float32, to use for doing the one-MinSize-per-layout algorithm for forms containing up to 16 rows, and switching to the allocation-free two-MinSize-per-layout algorithm here if the form is too large?

@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

Will #4681 work for user widgets too?

That is the plan, yes. I want it to work for BaseWidget directly so everyone can benefit from it and do avoid all the code duplication of our widgets doing their own caching.

I'd kind of consider that a necessity before thinking this trade off is worth it over using sync.Pool, but maybe it's just me.

It is a trade off. Do we want memory allocations (which can happen 60 times per second) in the fast path or do we want a slightly slower fast path? I'm kind of leaning towards that anyone with an incredibly slow MinSize function ought to be caching that value anyway.

@Jacalz Jacalz marked this pull request as ready for review May 2, 2024 15:46
@dweymouth
Copy link
Contributor

With sync.Pool, the mem allocations wouldn't happen 60 times a second. But take a look at my most recent comment (other than this one ;) - I think that could be an interesting angle

@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

It is also worth noting that there is a threshold where the performance of calling MinSize() twice with this code will be faster than the overhead of juggling the allocations and synchronisation overhead of sync.Pool together with the old code (depending on how fast MinSize is). But yeah, it is a trade off that needs to be thought about.

@dweymouth
Copy link
Contributor

The more I think about

Another approach could be to combine both methods and define a static array in the formLayout struct itself, like [16]float32, to use for doing the one-MinSize-per-layout algorithm for forms containing up to 16 rows, and switching to the allocation-free two-MinSize-per-layout algorithm here if the form is too large?

the more I like it. Switching between different algorithms for something depending on the input size is common in libraries going for performance. (Like using insertion sort for small lists/base cases and a faster algorithm for larger inputs.) This might be the first case of it in Fyne if we go with this approach here but I'm sure there are other places that could benefit from a similar approach as well.

@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

Oops. I missed that earlier message entirely before. Must have landed right as I commented. Sorry.

That is a really clever approach. I think Go developers generally don't use arrays often enough. Even stack allocating an array inside the function's call stack might be an option in some situations. It does however add a bit of extra complexity to keep track of the two buffers. I think the solution in this PR might still be the way to go, especially if I can get the caching of MinSize to work.

@dweymouth
Copy link
Contributor

dweymouth commented May 2, 2024

I think the solution in this PR might still be the way to go, especially if I can get the caching of MinSize to work.

We will agree to disagree :) At the least I would consider MinSize caching a hard prerequisite for this no-allocation solution without the separate array case for small forms

@dweymouth
Copy link
Contributor

I would, however, be good with merging this with the plan to do a follow-up that adds back the previous algorithm with a stack-allocated array for small form sizes

Copy link
Contributor

@dweymouth dweymouth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - with the plan to file a ticket to deal with the 2x MinSize calls in the small forms case with stack allocation

@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

I would, however, be good with merging this with the plan to do a follow-up that adds back the previous algorithm with a stack-allocated array for small form sizes

You mean using this algorithm for large forms and the old one (using stack allocated buffer) for small sizes? That sounds like a cool (and probably optimal/perfect) solution. It's worth noting that MinSize can easily be written to never allocate even with the old algorithm btw :)

@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

The fast path stack allocated array will likely not be necessary if MinSize caching lands though. I'll try to look at that PR again next week :)

@dweymouth
Copy link
Contributor

You're right it would be less necessary but still might eke out a tiny bit of speed improvement from not having to call as many non-inlineable functions. Plus there's no guarantee a custom widget will use BaseWidget and the caching vs completely implementing the interface from scratch. I think we can merge this and I may pick up the follow-up myself this evening

@Jacalz Jacalz merged commit 0f09fc6 into fyne-io:develop May 2, 2024
12 checks passed
@Jacalz Jacalz deleted the remove-form-allocs branch May 2, 2024 18:23
@Jacalz
Copy link
Member Author

Jacalz commented May 2, 2024

Alright then. Might be overcomplicating things. Make sure that you benchmark it well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants