Change:
Between and I experienced one of my binaries to grow up from 3.4M to 3.7M, which is approx 8.8%. At first I was scared that this was a fallout of my own PR (), but that particular binary didn't use C strings almost at all. I examined a diff in symbols made with and saw that the binary compiled with has a lot more symbols like that: (14 occurences) (57 occurences) (28 occurences) (mighty 138 new occurences) This leads us to the possibility that is the culprit. There are 2 reproducers, and I need to make some measurements:
The blowup of binary size is the minor difference. The main difference is in compilation times and run-times of that resulting binary, that are quite worse.
cc
can you enable us to reproduce this? I can investigate in parallel but bug reports tend to be much more readily fixable if there's a way to reproduce what caused the bug report!
Looks like this isn't a regression for "hello world", or likely just "in the noise"
I'll try to make a minimal reproducible example today or later this week
any updates on this? or do y'all have examples of this causing a size increase?
Unfortunately didn't have much time to dig into this. Will try as soon as I have possibility.
Here is a test case that may serve for exhibiting the space increase that observed around this time. (Or at least, on my Mac, building without optimizations, I am observing a size increase of about 1.08x on the stripped binaries. With optimizations, the size increase is about 1.05x) Build script: Results:
My previous comment notwithstanding, it would be really great if could provide us either with their original test case or a somewhat reduced version of it, since it is entirely possible that my (strawman) object size microbenchmark does not actually reflect the real issues that are underpinning the size increase that is observing.
a little increase in binary size is not important to me. But I am seeing significant increases in the run-time of the binary. I've filed a different issue but the cause could be the same:
oh note that I a bunch of in which may help with the perf related to that
This pathological case is producing ~8.5% increase in release mode between 2017-06-19 and 2017-06-23: : :
I have a datapoint to provide. For ease of experimentation, I made a [small variation][alexel] on alexbool's benchmark that avoids pulling in serde. (Basically, my goal was to have a single .rs file that could be independently compiled without any crate dependencies.) I think my benchmark is likely to get at the heart of what is problematic here (namely that the code-duplication from monomorphization of generics can amplify the effect of otherwise minor code size regressions). [alexel]: Manually inspecting the output assembly and comparing what we generated before and after the Allocator API landed, I saw cases where we could/should be inlining more. So I tried comparing against the branch that is basis for PR , which adds some directives we will be definitely needing to address execution time regressions. Unfortunately, that inlining happens to make the overall code size here worse. Another thing I noticed was that we seem to be spending a number of instructions just shuffling words around. It wasn't immediately obvious what all the causes were, but one clear candidate was the struct that now needs to be passed around when we return the case. So as an experiment, I tried making the struct zero-sized (i.e. removing all of its content, which is probably not what we want to do long term). I also inlines of and . rust variant | resulting code size (bytes) ------------|--------------------- just before Allocator API | just after Allocator API | post | post + making zero-sized + more inlines | So this shows that some of the code size regression can perhaps be blamed on the change to alloc's return type; it used to be , now it is (and I'm curious whether it goes down further if you make that , though we have already been through arguments against using here...) Update: I attempted to recreate my above results and discovered that I posted my original comment without finishing a crucial sentence: the 311,296 figure was gathered from a build that both made zero-sized and* marked more operations of as .
Thanks for the investigation and minimization I'm still pretty surprised about the increase in code there! I think it may be worth digging in a bit more to see where this "code bloat" is coming from. I would suspect that these are legitimately new locations for more validation, but it would be good to validate that claim itself. If no one else gets around to this I'll try to start investigating after lands
One thing that I keep seeing in the diffs regardless of how I attempt to "correct" for whatever changes that injected: there continues to be a big increase in the number of definitions. Building my benchmark just before the landed has 20 instantiated definitions of in its assembly output (many of them quite trivial and identical bits of code, sigh). Right after landed, there were 31 instantiated definitions of . But sometime after that, it blew up to 49 instantiated definitions. Not really sure when that happened.
Hm ok so now I'm getting interesting results again: Using as I get: However I get: Can this still be reproduced on the latest nightly?
Just a quick note for anyone trying to figure out how the numbers have been jumping up and down (at least, I was initially flummoxed when trying to compare 's results with the numbers I provided ) The original bug report was comparing against . So it was a bit confusing that used as a reference point, because the object size changed quite a bit between May 31st and June 18th, as illustrated here (where is the file came up with that lacks : In case its hard to see, here's the important sequence of numbers: 5/31 7/11 ----- ------ 450K 453K So, there was an observable regression (and it still seems to be present), but it is potentially masked by other improvements to code size that happened between May 31st and June 18th.
Oh I was actually just picking random dates, looks like I "got lucky"!
Also, another factor that is easy to overlook is that in the earlier comments from both and myself: our first invocations of had no additional options. The later invocations were , which only strips debugging symbols. The difference appears to be quite significant, at least on the binaries we are producing today (on a current nightly compiling hello world, the former yields a 348K binary, the latter yields a 432K binary).
In an effort to try to understand the size trajectories over time, I generalized the benchmark so that one can choose whether one wants to compile it with 1, 2, 3, 11, or 21 distinct variations on the type (and a corresponding number of vectors in the ), and then I compiled each variant with every nightly version of the compiler from May 1st through July 13th. My intention was to differentiate between a couple different things: How does the size of a trivial change over time? How much does that trivial program increase in size when you add the most basic instance of the that carries a single vector (with that same loop for filling its contents) How much does that program increase in size when you have two distinctly-typed vectors in ? How about three? As the number of variations of (and fields and instantiations) increases, what is the (amortized) cost per S? Here is a gist with the program, a shell script to drive it, and two sets of output transcripts (one that uses , another using ) Here are some observations based on looking both the development of the numbers over time (i.e. scanning down the columns) The numbers sometimes report no binary size increase when one just goes from two variations to three variations. (I assume this can be explained by the presence of padding between function definition(s) that allows code to increase in size "for free" sometimes.) This is probably a sign that I should switch to using rather than for the fine-grained analysis. Between and , the baseline cost of a trivial dropped quite a bit. This potentially masked later regressions, and its been creeping back up since then. Between and (i.e., when the API landed, plus some other things I have not yet identified), the amortized cost per type variation jumped from 409 bytes to 1638 bytes, That's when using ; with , the jump was from 510 bytes to (about) 2000 bytes.
Thanks for the scripts and investigation Lots of good data. This notably includes which probably just means that jemalloc 4.0.0 is a smaller binary than jemalloc 4.5.0 I tweaked the script/source file to account for this and always link to the system allocator and got which notably shows the same point of a regression you saw, and that the addition of didn't actually help and may have worsened it!
cc with away we're going to need some help tackling this one.
Needs an assignee. is away.
Discussed in compiler meeting. is going to attempt to get more profiling information.
So in terms of regressions I think one of the most actionable right now is the regression in compile time. If you take a look at the for when landed there's two regressions of note: One, the largest, happened from This is tracked by The second, smaller regression happened due to This is perhaps most notable when looking at the So taking a look specifically at the second regression, with some help from dmajor on IRC we've reached the conclusion that grew about 30% in the profiles.
The compiler team looked at this during triage. These are very interesting findings already! Maybe we can find out what exactly is slower in ? More detailed profiling of that would be interesting.
cc is this something you could help with?
I can try
Not sure if this adds to what's already known, but I've been testing a tool to compare DWARF debug info. I'm looking at the differences between nightly-2017-06-20 and nightly-2017-06-21 for the test case that gave using . The main size difference is for , which seems to be because we're inlining some function specializations instead of being able to call general allocator functions: There's also a large size difference in main: It's mostly due to changes in the size of the inlined : I can dig further into other compiler versions if this sort of info is useful.
Thanks for the info I think I'm noticing a few things with a fresh set of eyes, I'll send a PR soon.
I've opened with the results of micro-analyizing .
example had increased from 675k to 720k by the allocator PR, had a bit of upward creep, and was decreased from 732k to 723k by , so there's still much to improve.
I think someone needs to do a comparative analysis between the and versions - I bet much of that bloat is still left in today's rustc.
More accurate size statistics
So the sections that grew were:
OK. So, reasons for the bloat I've found: out an for takes code size. The previous instruction sequence was just (22 bytes) But the new instruction sequence is more of the form: (55 bytes) There are ~100 call-sites to , so this causes 3kB of bloat. and (in the old code) were marked as nounwind, but in the new code they are marked as unwind. I think this leads to most of the exception table bloat, and some code bloat. I remember seeing more inefficiencies, but I didn't write them down. I'll try to build a rustc with marked as and see how it fares.
I'm having some problems reproducing the exact sizes locally - I'll try more precisely tomorrow - but marking and as saved 24k of space (that's 50% of the overall increase) in one version.
I think I have a summary of the situation: The "jemalloc-related bloat" is an increase in the size of , and it seems to be caused by things like quarantine being enabled. It did not come with the first allocator PR. I'm not entirely sure what the unexplained bloat is or where it hides, but it's only 8k so I think I'll let it slide.
Thanks for the analysis It sounds to me though like there's not necessarily a huge amount to do here. The jemalloc bloat isn't something that should be a problem (small projects turn it off anyway) and the dealloc-related "unwind bloat" is more of a correctness fix right now than anything else. Also if projects care about binary size they're probably compiling with . That just leaves 8k of unexplained size along with 3k of OOM-related stuff, both of which seem quite reasonable and ripe for future optimization. In that sense with a lack of clear actionable bug, is it time to close this?
In which cases can unwind? Doesn't it just call the jemalloc function, which can never unwind?
So I think the 8k bloat was caused by other changes from to , and I don't think there's anything interesting left for me to investigate. I'm leaving this open for T-libs to decide what to do about it.
Seeing how no global allocators right now can unwind (system/jemalloc) I've proposed to fix the regression. I'll open a further issue for more discussion here related to unwinding allocators.
This is fixed on beta, removing from milestone.

Discussion:
///  * if a layout `k` fits a memory block (denoted by `ptr`) ///    currently allocated via an allocator `a`, then it is legal to ///    use that layout to deallocate it, i.e. `a.dealloc(ptr, k);`. <ins> /// /// # Unsafety /// /// The `Alloc` trait is an `unsafe` trait for a number of reasons, and /// implementors must ensure that they adhere to these contracts: /// /// * Pointers returned from allocation functions must point to valid memory and ///   retain their validity until at least the instance of `Alloc` is dropped ///   itself. /// /// * It's undefined behavior if global allocators unwind.  This restriction may ///   be lifted in the future, but currently a panic from any of these ///   functions may lead to memory unsafety. Note that as of the time of this ///   writing allocators *not* intending to be global allocators can still panic ///   in their implementation without violating memory safety. /// /// * `Layout` queries and calculations in general must be correct. Callers of ///   this trait are allowed to rely on the contracts defined on each method, ///   and implementors must ensure such contracts remain true. /// /// Note that this list may get tweaked over time as clarifications are made in /// the future. Additionally global allocators may gain unique requirements for /// how to safely implement one in the future as well. </ins> pub unsafe trait Alloc { // (Note: existing allocators have unspecified but well-defined

extern "Rust" { #[allocator] <ins> #[rustc_allocator_nounwind] </ins> fn __rust_alloc(size: usize, align: usize, err: *mut u8) -> *mut u8; #[cold] <ins> #[rustc_allocator_nounwind] </ins> fn __rust_oom(err: *const u8) -> !; <ins> #[rustc_allocator_nounwind] </ins> fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize); <ins> #[rustc_allocator_nounwind] </ins> fn __rust_usable_size(layout: *const u8, min: *mut usize, max: *mut usize); <ins> #[rustc_allocator_nounwind] </ins> fn __rust_realloc(ptr: *mut u8, old_size: usize, old_align: usize, new_size: usize, new_align: usize, err: *mut u8) -> *mut u8; <ins> #[rustc_allocator_nounwind] </ins> fn __rust_alloc_zeroed(size: usize, align: usize, err: *mut u8) -> *mut u8; <ins> #[rustc_allocator_nounwind] </ins> fn __rust_alloc_excess(size: usize, align: usize, excess: *mut usize, err: *mut u8) -> *mut u8; <ins> #[rustc_allocator_nounwind] </ins> fn __rust_realloc_excess(ptr: *mut u8, old_size: usize, old_align: usize,

new_align: usize, excess: *mut usize, err: *mut u8) -> *mut u8; <ins> #[rustc_allocator_nounwind] </ins> fn __rust_grow_in_place(ptr: *mut u8, old_size: usize, old_align: usize, new_size: usize, new_align: usize) -> u8; <ins> #[rustc_allocator_nounwind] </ins> fn __rust_shrink_in_place(ptr: *mut u8, old_size: usize, old_align: usize,

#![feature(pattern)] #![feature(placement_in_syntax)] #![feature(placement_new_protocol)] <ins> #![feature(rustc_attrs)] </ins> #![feature(shared)] #![feature(slice_get_slice)] #![feature(slice_patterns)]

llvm::AttributePlace::ReturnValue(), llfn); } else if attr.check_name("unwind") { unwind(llfn, true); <ins> } else if attr.check_name("rustc_allocator_nounwind") { unwind(llfn, false); </ins> } } if !target_features.is_empty() {

<ins> // Copyright 2017 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your // option. This file may not be copied, modified, or distributed // except according to those terms. // // no-system-llvm // compile-flags: -O #![crate_type="lib"] struct A; impl Drop for A { fn drop(&mut self) { extern { fn foo(); } unsafe { foo(); } } } #[no_mangle] pub fn a(a: Box<i32>) { // CHECK-LABEL: define void @a // CHECK: call void @__rust_dealloc // CHECK-NEXT: call void @foo let _a = A; drop(a); } </ins>