-
Notifications
You must be signed in to change notification settings - Fork 256
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
Binary size optimization experiment #569
Conversation
35747a9
to
e007e79
Compare
src/macros.rs
Outdated
let lvl = $lvl; | ||
if lvl <= $crate::STATIC_MAX_LEVEL && lvl <= $crate::max_level() { | ||
match (::std::format_args!($($arg)+), $target, $kvs) { | ||
(args, target, kvs) => if let ::std::option::Option::Some(literal) = ::std::fmt::Arguments::as_str(&args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary, since it is turned into an fmt::Argument<'_>
again when constructing Record<'_>
.
I think this actually prevents the Log
implementation to take advantage of Argument::as_str
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is that passing an Arguments
value is more costly than passing an str
value: https://godbolt.org/z/bzbnfq1TM. I expect an application may contain a lot of logs that only contains string literals, so if we can reduce the cost of each log call, the total binary size could been reduced. Essentially, this implementation tries to bring back an old optimization that was reverted by #446.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can pass a reference to Arguments, then dereference it when building Records.
It implements Copy, so you can avoid copying it in each function while preserving the ability to take advantages of Arguments::as_str
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aside from copying, building an Argument
object also has costs: https://godbolt.org/z/a1eqr4bq5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that passing Arguments
and passing &Arguments
have the same cost: https://godbolt.org/z/Wz35xYrsr.
Is this PR trying to take advantages of rust-lang/rust#109999 ? |
Not exactly. This PR intends to minimize code generated by macro expansion. This is done by moving as much code logic as possible into functions that can be shared by each macro call. |
Thanks for explanation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1ba35e5
to
93eed72
Compare
93eed72
to
1b59e61
Compare
It seems that using "opt-level=z", the compiler won’t inline Maybe marking |
You could just pass |
@NobodyXu: I have tested the effect of passing |
Your linked code does not actually use the Arguments, so the compiler might omits the copy requires to happen. In your PR, you passes the Arguments to log2, log1, log0 by value, that would require copying Arguments between stacks unless inlined. |
|
ac32187
to
14c70e5
Compare
14c70e5
to
81dd465
Compare
OK, I think this is ready for the next step. This PR is just a proof of concept of some ideas I have, and should not be merged as is. I have tested this PR on my personal project and gained about 2.4% binary size reduction, which is significant to me. Now I want to implement these ideas in the
Also, is there a known open source project that uses |
In
|
It seems that |
It uses many dependencies which depends on If you check the "details" which contained the dependency tree, you would find it actually heavily depends on |
module_path_and_file: &'static (&'static str, &'static str), | ||
line: u32, | ||
level: L, | ||
args: A, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could try passing &Arguments<'_>
here and in all log_*
instead of passing generics here.
I still think losing Arguments::as_str
as an optimization is bad and passing it by reference could make up for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a const function to check whether the format argument is a string literal as a not perfect replacement for Arguments::as_str
, that’s why I used a generic argument here. Sometimes I need to pass &str
as args
.
As for &Arguments
, I think I should do some test, then decide which one to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a const function to check whether the format argument is a string literal as a not perfect replacement for
Arguments::as_str
I was actually referring to using Argument::as_str
in Log::log
, the implementation might be able to avoid heap alloc using Argument::as_str
, passing &str
here breaks that optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand. Log::log
is not implemented by this crate, so how do I utilize Argument::as_str
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand.
Log::log
is not implemented by this crate, so how do I utilizeArgument::as_str
?
Implementator of Log::log
could use Argument::as_str
to avoid heap allocation.
Now that you decided to pass arg as &str
if possible, they can no longer utilize this optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point. Maybe this can be optimized by format_args!
as a special case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point. Maybe this can be optimized by
format_args!
as a special case?
Ideally yes, but that will require the args to be &'static str
, which involves lifetime and is a bit hard to do.
I guess we can ask @m-ou-se for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simpler approach could be impl<'a> From<&'a &'static str> for Arguments<'a>
, utilizing Arguments::new_const
.
src/__private_api.rs
Outdated
|
||
impl LogKVs for &[(&str, &LogKvValue<'_>)] { | ||
#[inline] | ||
fn with(self, f: impl FnOnce(&[(&str, &LogKvValue)])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why every Log*
trait has to accept a closure here?
Can't they just return the value directly?
Returning the closure would only add more overhead and generate more code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For LogArgs
, I can’t return a Arguments
value from a &str
value:
fn f(s: &str) -> Arguments<'_> {
format_args!("{s}") // Does not work because a local variable reference is captured.
}
So I have to use a callback function.
For other ones, since they need to be converted into references, the trait method may need to take a &self
argument, and I don’t feel like taking references from &str
or a zero sized type, since the will get a &&str
type and &ZST
type. But I am not very insistent on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I have to use a callback function.
Yeah for LogArgs
you have to use a callback due to limitation of Arguments<'_>
, although I wonder
For other ones, since they need to be converted into references, the trait method may need to take a
&self
argument, and I don’t feed like taking references from&str
or a zero sized type, since the will get a&&str
type and&ZST
type. But I am not very insistent on this.
I don't think there's much issue with this since it will most likely be inlined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, now I have removed callback functions from LogTarget
and LogKVs
.
6ebe92e
to
3fa1048
Compare
I have tests this PR on cargo-binstall, and here is the binary size comparison:
You can reproduce the result using this program. |
Thanks for investigating this @EFanZh! These were a useful point-in-time reference that helped make some improvements, but I think we should close now that things have moved along. Please feel free to open any fresh PRs that optimize binary size based on the current state of the code though. |
Experiment for some ideas I have on binary size optimization.