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

Optimise atom_manager! implementation #914

Merged
merged 2 commits into from
Feb 15, 2024
Merged

Conversation

psychon
Copy link
Owner

@psychon psychon commented Feb 3, 2024

winit uses atom_manager! with 59 atom names. Due to some sub-optimal code generation, this causes the generated new() function to end up with a size of 111.6 KiB, or almost 5% of the .text size of a winit example.

The issue is that each "intern_atom()?" can cause an early exit and needs to call drop() of the already generated Cookies. This leads to some quadratic growth.

To work around this, in this commit I change the implementation of new(). It now iterates over the atom names, using .map() to send the InternAtom requests. Finally, collect() is then used to change this Iterator<Item=Result<Cookie, Error>> into Result<Vec, Error>. This even preserves the early-exit behaviour in case of errors.

The reply() function is then changed to turn the Vec into an iterator, using next() to pull out the items. This relies on the evaluation of this code to be well-defined (which e.g. wouldn't be guaranteed in C). Luckily, Rust provides the needed guarantee:
rust-lang/reference#888

Fixes: #912


I tried to measure this with latest winit master:

$ cargo build --release --no-default-features --features x11 --example video_modes
$ ls -lh target/release/examples/video_modes
$ strip target/release/examples/video_modes
$ ls -lh target/release/examples/video_modes

With the latest x11rb release, the result is 16 MB / 2,0 MB. With this change (copy&paste the macro into the winit code), it is 15 MB / 2.0 MB.

winit uses atom_manager! with 59 atom names. Due to some sub-optimal
code generation, this causes the generated new() function to end up with
a size of 111.6 KiB, or almost 5% of the .text size of a winit example.

The issue is that each "intern_atom()?" can cause an early exit and
needs to call drop() of the already generated Cookies. This leads to
some quadratic growth.

To work around this, in this commit I change the implementation of
new(). It now iterates over the atom names, using .map() to send the
InternAtom requests. Finally, collect() is then used to change this
Iterator<Item=Result<Cookie, Error>> into Result<Vec<Cookie>, Error>.
This even preserves the early-exit behaviour in case of errors.

The reply() function is then changed to turn the Vec into an iterator,
using next() to pull out the items. This relies on the evaluation of
this code to be well-defined (which e.g. wouldn't be guaranteed in C).
Luckily, Rust provides the needed guarantee:
rust-lang/reference#888

Fixes: #912
Signed-off-by: Uli Schlachter <psychon@znc.in>
Copy link

codecov bot commented Feb 3, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (0fbfd89) 13.06% compared to head (39be186) 13.07%.
Report is 2 commits behind head on master.

❗ Current head 39be186 differs from pull request most recent head f431b67. Consider uploading reports for the commit f431b67 to get more accurate results

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #914   +/-   ##
=======================================
  Coverage   13.06%   13.07%           
=======================================
  Files         190      190           
  Lines      136667   136674    +7     
=======================================
+ Hits        17859    17870   +11     
+ Misses     118808   118804    -4     
Flag Coverage Δ
tests 13.07% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator

@notgull notgull left a comment

Choose a reason for hiding this comment

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

LGTM aside from some macro hygeine

$field_name: $crate::cookie::Cookie<'a, C, $crate::protocol::xproto::InternAtomReply>,
)*
__private_phantom: std::marker::PhantomData<&'a C>,
__private_cookies: Vec<$crate::cookie::Cookie<'a, C, $crate::protocol::xproto::InternAtomReply>>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
__private_cookies: Vec<$crate::cookie::Cookie<'a, C, $crate::protocol::xproto::InternAtomReply>>,
__private_cookies: ::std::vec::Vec<$crate::cookie::Cookie<'a, C, $crate::protocol::xproto::InternAtomReply>>,

Just in case a potential user defines their own Vec struct.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks for catching that! I turned the existing tests/x11_utils.rs into a test case for that and also found a std:: which should be ::std::.

let names = [
$($crate::__atom_manager_atom_value!($field_name$(: $atom_value)?),)*
];
let cookies: Result<Vec<_>, _> = names.into_iter().map(|name| _conn.intern_atom(false, name)).collect();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let cookies: Result<Vec<_>, _> = names.into_iter().map(|name| _conn.intern_atom(false, name)).collect();
let cookies: ::std::result::Result<::std::vec::Vec<_>, _> = names.into_iter().map(|name| _conn.intern_atom(false, name)).collect();

let names = [
$($crate::__atom_manager_atom_value!($field_name$(: $atom_value)?),)*
];
let cookies: Result<Vec<_>, _> = names.into_iter().map(|name| _conn.intern_atom(false, name)).collect();
Copy link
Collaborator

Choose a reason for hiding this comment

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

 _conn.intern_atom(

Should we import the x11rb::protocol::xproto::ConnectionExt trait here, in case the user hasn't already?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Uhm... this doesn't need ConnectionExt?

I wanted to make x11rb/tests/x11_utils.rs test for this, but it already does. It does not import ConnectionExt.

If I had to guess, I would say the bound C: $crate::protocol::xproto::ConnectionExt brings the trait into scope.

@yshui
Copy link

yshui commented Feb 5, 2024

Interesting find! I wonder if rustc is being particularly dumb here or if, say, g++ has the same behaviour 🤔

@psychon
Copy link
Owner Author

psychon commented Feb 5, 2024

@yshui FYI, I reported this upstream: rust-lang/rust#120604
Dunno how I could test something precisely like this with G++. There is not really an equivalent for ? in C++ and stuff is quite different in general.

Here is an attempt.

Link to godbolt

#include <optional>

struct Dropper {
    Dropper();
    ~Dropper();
};

bool choice();

struct Drops {
    Dropper a, b;
#ifdef DO_C
    Dropper c;
#endif
#ifdef DO_D
    Dropper d;
#endif
};

std::optional<Drops> __attribute__((noinline)) do_it() {
    Dropper a;
    if (choice()) { return std::nullopt; }
    Dropper b;
    if (choice()) { return std::nullopt; }
#ifdef DO_C
    Dropper c;
    if (choice()) { return std::nullopt; }
#endif
    return Drops {
        .a = std::move(a),
        .b = std::move(b),
#ifdef DO_C
        .c = std::move(c),
#endif
#ifdef DO_D
        .d = std::move(d),
#endif
    };
}

Without #defines, there are four calls to ~Dropper in do_it. With DO_C, there are six calls. DO_C plus DO_D produces eight calls.

That seems linear to me.

This makes sure that the macro refers to standard types in an
unambiguous way that does not use types from the local module. Instead,
a full path beginning with "::std" is used.

Thanks to @notgull for pointing this out.

Signed-off-by: Uli Schlachter <psychon@znc.in>
@yshui
Copy link

yshui commented Feb 5, 2024

@psychon slightly tweaked your example so it's a bit clear: https://godbolt.org/z/GqWcPn9bs

so g++ indeed does what i expect here, it's essentially:

    Dropper a;
    if (cond1) goto free1;
    Dropper b;
    if (cond2) goto free2;
    Dropper c;
    if (cond3) goto free3;

    //....

free3:
    delete c;
free2:
    delete b;
free1:
    delete a;
    return

definitely something worth fixing in Rust. I am not familiar with rustc though....

@yshui
Copy link

yshui commented Feb 5, 2024

ah but in Rust's case it's more complex because some variables could have been moved thus don't need dropping.

@psychon psychon requested a review from notgull February 15, 2024 20:22
@mergify mergify bot merged commit 6592b38 into master Feb 15, 2024
21 checks passed
@mergify mergify bot deleted the atom-manager-binary-size branch February 15, 2024 20:27
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.

x11rb::atom_manager's output size is very large
3 participants