We, at Google, recently evaluated Facebook’s BOLT, a Post Link Optimizer
framework, on large google benchmarks and noticed that it improves key
performance metrics of these benchmarks by 2% to 6%, which is pretty impressive
as this is over and above a baseline binaryalready heavily optimized with
ThinLTO + PGO. Furthermore, BOLT is also able to improve the performance of
binaries optimized via Context-Sensitive PGO. While ThinLTO + PGO is also
profile guided and does very aggressive performance optimizations, there is
more room for performance improvements due to profile approximations while
applying the transformations. BOLT uses exact profiles from the final binary
and is able to fill the gaps left by ThinLTO + PGO. The performance
improvements due to BOLT come from basic block layout, function reordering and
function splitting.
While BOLT does an excellent job of squeezing extra performance from highly
optimized binaries with optimizations such as code layout, it has these major
issues:
* It does not take advantage of distributed build systems.
* It has scalability issues and to rewrite a binary with a ~300M text segment
size:
* Memory foot-print is 70G.
* It takes more than 10 minutes to rewrite the binary.
Similar to Full LTO, BOLT’s design is monolithic as it disassembles the
original binary, optimizes and rewrites the final binary in one process. This
limits the scalability of BOLT and the memory and time overhead shoots up
quickly for large binaries.
Inspired by the performance gains and to address the scalability issue of BOLT,
we went about designing a scalable infrastructure that can perform BOLT-like
post-link optimizations. In this RFC, we discuss our system, “Propeller”,
which can perform profile guided link time binary optimizations in a scalable
way and is friendly to distributed build systems. Our system leverages the
existing capabilities of the compiler tool-chain and is not a stand alone tool.
Like BOLT, our system boosts the performance of optimized binaries via
link-time optimizations using accurate profiles of the binary. We discuss the
Propeller system and show how to do the whole program basic block layout using
Propeller.
Propeller does whole program basic block layout at link time via basic block
sections. We have added support for having each basic block in its own section
which allows the linker to do arbitrary reorderings of basic blocks to achieve
any desired fine-grain code layout which includes block layout, function
splitting and function reordering. Our experiments on large real-world
applications and SPEC with code layout show that Propeller can optimize as
effectively as BOLT, with just 20% of its memory footprint and time overhead.
An LLVM branch with propeller patches is available in the git repository here:
https://github.com/google/llvm-propeller/ We will upload individual patches of
the various elements for review. We have attached a google doc describing the
Propeller system with Experiments in detail,
https://github.com/google/llvm-propeller/blob/plo-dev/Propeller_RFC.pdf
Quick Start - How to optimize further with Propeller?
# git clone and build repo
$ cd $LLVM_DIR && git clone https://github.com/google/llvm-propeller.git
$ mkdir $BUILD_DIR && cd $BUILD_DIR
$ cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang;lld;compiler-rt" … \
$LLVM_DIR/llvm-propeller/llvm
$ ninja -j25
$ export PATH=$BUILD_DIR/bin:$PATH
# Let’s Propeller-optimize the following program:
# Step 1: Build the peak optimized binary with an additional flag.
$ clang++ -O2 main.cc callee.cc -fpropeller-label -o a.out.labels -fuse-ld=lld
# Step 2: Profile the binary, only one side of the branch is executed.
$ perf record -e cycles:u -j any,u -- ./a.out.labels 1000000 2 >& /dev/null
# Step 3: Convert the profiles using the tool provided
$ $LLVM_DIR/llvm-propeller/create_llvm_prof --format=propeller \
--binary=./a.out.labels --profile=perf.data --out=perf.propeller
# Step 4: Re-Optimize with Propeller, repeat Step 1 with propeller flag changed.
$ clang++ -O2 main.cc callee.cc -fpropeller-optimize=perf.propeller -fuse-ld=lld
In Step 4, the optimized bit code can be used if it is saved in Step1 as
Propeller is active only during compile backend and link. The optimized binary
has a different layout of the basic blocks in main to keep the executed blocks
together and split the cold blocks.
Some of the key points:
+ Added support for basic block sections, similar to function sections, where
each basic block can reside in its own section.
+ Jump instructions need 32-bit relocations and subsequent linker relaxations
after basic block layout. We would like to add a new relocation type for jump
instructions to make it easier for relaxations and guarantee correctness.
+ Added support in the linker to read profiles (PMU LBR) and discover optimal
basic block layout using the Ex-TSP algorithm described here:
https://arxiv.org/abs/1809.04676
+ Added support in the linker to re-order basic block sections in any
specified sequence (can be done with symbol ordering file). This requires
linker relaxations to delete and shrink branches across basic blocks.
+ Compared our system to BOLT and have shown that our system can produce
similar performance improvements as BOLT but with much less memory and time
overheads. Our experiments are on very large warehouse-scale benchmarks and
SPEC 2017.
+ Listed why this cannot be done as part of PGO itself. Post Link
optimizations are able to transform the binary using accurate profiles and PGO
passes suffer from profile imprecision.
+ Updated DebugInfo and CFI to account for arbitrary ordering of basic blocks
via basic block sections.
+ Discussed CFI handling and is sub-optimal and bloats object file sizes and
binary sizes much more than DebugInfo due to lack of support for discontiguous
address ranges. We have talked about this and would like to make a case to
support discontiguous ranges with CFI which will make basic block sections much
more cheaper.
Detailed RFC document here :
https://github.com/google/llvm-propeller/blob/plo-dev/Propeller_RFC.pdf
Please let us know what you think,
Thanks
Sri on behalf of the team.
_______________________________________________
LLVM Developers mailing list
llvm...@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev
I have mentioned this in the RFC under alternate design in the
Experments. We did consider this approach. This is our first attempt
at doing this end to end. We plan to do much more. We are looking at
much more aggressive inter-procedural code layout optimizations based
on path profiles and machine learning, which requires having support
for basic block sections. Being able to do inter-procedural block
layout is already showing more performance improvements over what we
have right now. We don't think basic block sections is expensive if
done on demand and there are opportunities for performance exploration
with this support.
My biggest question about this architecture is about when propeller runs basic block reordering within a function. It seems like a lot of the complexity comes from using the proposed -fbasicblock-sections to generated mangled ELF, and then re-parsing the mangled ELF as a separate step. I'm not sure that's the right approach, long-term.
Splitting every basic block into its own section introduces overhead, like you described. And it's likely more complex on non-x86 targets, which have a greater variety of conditional branches. And the reordering itself involves a bunch of x86 and ELF-specific code.
I'd like to suggest an alternative: instead of perform basic block reordering and function splitting by manipulating the ELF files, you could perform reordering and splitting as part of link-time code generation, as an MIR pass that runs just before the assembly printer. MIR is almost exactly the form you want for this sort of manipulation: it has basic blocks which correspond closely to the final binary, and a high-level representation of branch instructions.
And it's before the DWARF/CFI emission, so you don't need to worry about fixing them afterwards. This should take less code overall, and much less target-specific code. And infrastructure for function splitting would be useful for non-Propeller workflows.
There are some minor downsides to this approach I can think of. You lose a little flexibility, in that you can't mix blocks from different functions together, but you aren't doing that anyway, from your description?
Long term, we are looking at inter-procedural basic block layout,
basic block cloning etc. and this encapsulates block layout, function
splitting and function reordering. We will look at how we could
integrate this with existing callgraph layout algorithm.
>
> - Michael Spencer
We looked at the code and it looks like the function reordering parts
are using the hfsort algorithm, both CallgraphSort and Propeller.
So, we can merge these. When callgraphsort option is passed,
Propeller will invoke the hfsort code to do function layout. We
prefer doing this as a follow-up patch if that is alright as this
requires more plumbing.
>
> I would expect that whichever ordering algorithm is best would be the best for both profile sources, so I would recommend looking at integrating the function reordering code with the existing code instead of adding a separate one.
>
> - Michael Spencer
From: Xinliang David Li <xinli...@gmail.com>
Sent: Wednesday, September 25, 2019 5:58 PM
To: Eli Friedman <efri...@quicinc.com>
Cc: Sriraman Tallam <tmsr...@google.com>; llvm-dev <llvm...@lists.llvm.org>
Subject: [EXT] Re: [llvm-dev] [RFC] Propeller: A frame work for Post Link Optimizations
On Wed, Sep 25, 2019 at 5:02 PM Eli Friedman via llvm-dev <llvm...@lists.llvm.org> wrote:
My biggest question about this architecture is about when propeller runs basic block reordering within a function. It seems like a lot of the complexity comes from using the proposed -fbasicblock-sections to generated mangled ELF, and then re-parsing the mangled ELF as a separate step. I'm not sure that's the right approach, long-term.
Splitting every basic block into its own section introduces overhead, like you described. And it's likely more complex on non-x86 targets, which have a greater variety of conditional branches. And the reordering itself involves a bunch of x86 and ELF-specific code.
I'd like to suggest an alternative: instead of perform basic block reordering and function splitting by manipulating the ELF files, you could perform reordering and splitting as part of link-time code generation, as an MIR pass that runs just before the assembly printer. MIR is almost exactly the form you want for this sort of manipulation: it has basic blocks which correspond closely to the final binary, and a high-level representation of branch instructions.
This was considered for Propeller. This is currently being explored in a similar way as an alternative of CSFDO which uses PMU samples.
Makes sense.
And it's before the DWARF/CFI emission, so you don't need to worry about fixing them afterwards. This should take less code overall, and much less target-specific code. And infrastructure for function splitting would be useful for non-Propeller workflows.
There are some minor downsides to this approach I can think of. You lose a little flexibility, in that you can't mix blocks from different functions together, but you aren't doing that anyway, from your description?
One of the main design objectives of Propeller is to have the capability to do interprocedural code transformations (reordering, cloning, dedupping etc), so this won't be a minor downside. Function/block alignment (for branch misprediction reduction etc) will also need to be done as a global optimization in the future.
Okay, so my suggestion doesn’t work. I’m still concerned the proposed design is going to push us in a direction we don’t want to go. Particularly, if you’re going to attempt more complicated transforms, the problems caused by the limited information available in an ELF file will become more prominent. I mean, yes, you can come up with a more complicated implicit contract between the compiler and Propeller about the exact format of Propeller basic blocks, and add additional symbol annotations, and eventually come up with an “IR” that allows Propeller to perform arbitrary code transforms. But that’s inevitably going to be more complicated, and harder to understand, than a compiler IR designed for optimizations.
If we are going to stick with ELF-as-compiler-IR, I’d like to see more careful documentation of the contract between the compiler and the Propeller linker, so it’s clear what the compiler is/is not allowed to emit at compile-time, and so other compilers could be made compatible in the future. The current Propeller implementation is doing some code rewrites that can’t be justified by just standard ELF section reordering.
-Eli
Thanks for the feedback, I am not sure I fully understand your
concerns but let me try to make some of the things clearer:
* Propeller relinks. Specifically, it regenerates ELF object files
from MIR. Even if MIR were serializable, we would still be starting
before CFI instruction inserter pass and then regenerate the native
ELF objects.
* All the code transformations we are planning to do for futuristic
optimizations is on the MIR just like you noted in a previous email.
For example, prefetch insertion was one optimization we were looking
at where the input is inserting prefetches at specific points in the
binary which we will translate to basic blocks at MIR and then insert
them there. So, I don't think we will be limited by ELF files.
* In comparison with BOLT, BOLT disassembles to MCInst to apply some
of these optimizations and we get to start from MIR without the cost
of disassembly.
* I think your concern is coming from looking at the linker
relaxations we perform for X86_64 and wondering if this would scale
for RISC, etc. We have not looked at RISC but chatting with Rui
(ruiu@) briefly, this looked like it is doable even without using
thunks.
Thanks
Sri
>
>
>
> If we are going to stick with ELF-as-compiler-IR, I’d like to see more careful documentation of the contract between the compiler and the Propeller linker, so it’s clear what the compiler is/is not allowed to emit at compile-time, and so other compilers could be made compatible in the future. The current Propeller implementation is doing some code rewrites that can’t be justified by just standard ELF section reordering.
>
>
>
> -Eli
TLDR; We are regenerating ELF files from optimized IR to keep the
cost of generating basic block sections low.
If what you describe above is done, where we generate basic block
sections even before we profile, we must conservatively generate
sections for all functions. This is unnecessarily expensive and we
have shown that generating it on demand based on profiles is much
cheaper. Since the backend generation can be distributed or
parallelized, this is not expensive to do. If we could make the
cost of basic block sections in terms of size bloats cheaper we could
do what you just suggested here, that is, always build with basic
block sections for all basic blocks.
>
> I thought the workflow was something like the following:
>
> 1. The compiler (or LTO codegen) generates ELF files with basic block sections.
The correction here is that we generate first with basic block labels
and not sections.
> 2. You link once without propeller optimizations.
This is correct.
> 3. You collect the propeller profile.
This is correct.
> 4. You use the same ELF files to relink with propeller optimizations.
We use the same optimized IR files to regenerate the ELF files. We
could use optimized MIR here but serializability of MIR is not well
supported yet and is part of the larger plan. We only need to re-run
CFI instruction inserter in MIR. For future optimizations, we can
plug in here and make small code transformations for optimizations
like prefetch insertion. If we attempt to do basic block re-ordering
here, we would be limited to intra-procedural basic block reordering
which is sub-optimal. Hence, we generate basic block sections here
only for the sampled functions which is a small %age.
>
> That's sufficient to implement all the optimizations you described, as far as I can tell.
>
> But it sounds like that's wrong? It sounds like what you're describing is:
>
> 1. LTO codegen generates MIR files.
> 2. You convert the MIR files to ELF object files, and link without propeller optimizations. (Does this step have any propeller-specific code generation differences?)
Yes, like I said above, basic block labels are used here to identify
the basic blocks which are later used in the last step to annotate
profiles.
> 4. You collect the propeller profile.
> 5. You apply some propeller optimization to the MIR files.
> 6. You convert the optimized MIR files to ELF object files, with basic block sections enabled.
Right, technically this is high level IR now as MIR is not
serializable but this is ok for discussion.
> 7. You link the ELF files, applying propeller reordering optimizations.
>
> (And since you can't serialize MIR, you actually serialize LLVM IR, and run the code generator when you deserialize, to convert that to MIR.)
>
> Is that correct?
That is correct.
>
> If you have the MIR at the time you're making all the propeller optimization decisions, why is the linker rewriting raw x86 assembly, as opposed to performing the relevant transforms on MIR?
MIR is still one module at a time. We cannot do inter-procedural
basic block layout here. We can do much more advanced stuff at the
whole-program level in the linker. The relaxation code is the down
side.
For more details, We strongly considered this. We could run something
like a thin link in thin lto figure out the global layout and hand out
the relevant subsets of the global decision to each module. This
looked more complicated and the individual pieces from each module
should still be globally laid out again by the linker. This
constraints us on what we can do for layout and also does not work
well with future optimizations like global alignment like David
pointed out.
>
> Why are you proposing to add a bunch of options to clang to manipulate LLVM code generation, given none of those options are actually used for Propeller workflows?
Where do you suggest labelling and section options should exist? We
looked at basic block sections to be similar to function sections in
terms of option handling?
Thanks
Sri
MIR is still one module at a time. We cannot do inter-procedural
basic block layout here. We can do much more advanced stuff at the
whole-program level in the linker. The relaxation code is the down
side.
For more details, We strongly considered this. We could run something
like a thin link in thin lto figure out the global layout and hand out
the relevant subsets of the global decision to each module. This
looked more complicated and the individual pieces from each module
should still be globally laid out again by the linker.
This
constraints us on what we can do for layout and also does not work
well with future optimizations like global alignment like David
pointed out.
>
> Why are you proposing to add a bunch of options to clang to manipulate LLVM code generation, given none of those options are actually used for Propeller workflows?
>
> If you have the MIR at the time you're making all the propeller optimization decisions, why is the linker rewriting raw x86 assembly, as opposed to performing the relevant transforms on MIR?
MIR is still one module at a time. We cannot do inter-procedural
basic block layout here. We can do much more advanced stuff at the
whole-program level in the linker. The relaxation code is the down
side.
For more details, We strongly considered this. We could run something
like a thin link in thin lto figure out the global layout and hand out
the relevant subsets of the global decision to each module. This
looked more complicated and the individual pieces from each module
should still be globally laid out again by the linker. This
constraints us on what we can do for layout and also does not work
well with future optimizations like global alignment like David
pointed out.
Thanks,Kristof
Yes, this is correct. Relaxation is needed because the layout is not
finalized until link time.
And you *only* need to rewrite branches this way; never anything else,
because other instructions don't need relaxation?
I cannot think of any other instance where such relaxation is needed.
>
> On most targets, we do branch relaxation in the compiler, unlike x86 which does it in the assembler. I guess we could teach the compiler to emit some specific "relaxed" pattern, and teach the linker to reverse it. Might be a little tricky to handle certain odd cases well; currently, for example, on AArch64 we sometimes insert a cpsr clobber as part of relaxation. I'm not really happy with having to reimplement that for every target, but if we're sure it's limited to branches, I guess it's okay.
Understood.
>
> > > Why are you proposing to add a bunch of options to clang to manipulate LLVM
> > code generation, given none of those options are actually used for Propeller
> > workflows?
> >
> > Where do you suggest labelling and section options should exist? We
> > looked at basic block sections to be similar to function sections in
> > terms of option handling?
>
> Generating bitcode with/without propeller doesn't actually affect the generated bitcode, right? So you could say that Propeller is enabled with "-Wl,--enable-propeller", and not change clang at all. I'm not a fan of adding options just because we can. If basic block sections are only used as a sort of secret handshake between the propeller compiler and the propeller linker, we can change that interface later, if we want; if we expose it as a clang option, we're committing to making basic block sections continue to work the same way until the end of time.
The generated MIR code is slightly different as the later passes have
more CFI instructions, basic block labels and extra direct jumps which
would have been fall throughs otherwise. So, some llvm option is
needed.
I envisioned that basic block sections could also be useful on its own
outside of propeller. Hence, we made it like function-sections with
a separate option -fbasicblock-sections. The propeller option is an
umbrella option to make it easy to invoke Propeller.
Thanks
Sri
I see what you mean here.
>
> > I envisioned that basic block sections could also be useful on its own
> > outside of propeller. Hence, we made it like function-sections with
> > a separate option -fbasicblock-sections. The propeller option is an
> > umbrella option to make it easy to invoke Propeller.
>
> I don't think this is really true. Basic block labels means "insert labels that are useful for propeller profiling", and basic block sections means "split the function into multiple chunks that are convenient for propeller optimization". So it's really only useful for propeller-like workflows. And I'd be concerned that future changes to propeller could affect the behavior of these options in the future, if the behavior isn't specified in some rigorous way.
The idea of basic block sections was seeded by Rui Ueyama. When basic
block sections was originally looked at, Propeller was not designed.
We looked at basic block sections as finer granularity than function
sections. Section prefix based code layout where you just create
smaller code sections and let the linker do what it does today would
have much better results with basic block sections. Symbol ordering
file with basic block sections to do random orderings can be done
without invoking Propeller. Function sections has found uses after it
was invented like Identical Code Folding.
>
> In any case, if we're adding flags to clang other than "enable propeller", I think we should have a separate thread on cfe-dev to discuss them. We don't expose every possible LLVM optimization and code generation option through clang. Users have a strong expectation of stability for clang options, and that means we need to evaluate each new option carefully, to decide whether it's something we really want to support indefinitely.
Understood.
Thanks
Sri
I'm not sure that function sections are entirely an apt comparison
here for a few reasons:
- function sections mostly gave an easy way to do ICF - using MIR or
LLVM IR are also fairly effective means as is just using the linker to
compare bytes. Using function sections can still run into language
specific problems as well, hence why icf=safe vs icf=all. Though pcc's
recent work helps out a little bit via significant address
determination in the front end
- order files and other similar mechanisms work based on externally
visible symbols and are fairly straightforward to use in an ABI safe
way (no function -> function fallthrough for example)
- basic block sections can run into the multiple entry point problems
depending on how they're laid out as well as other ABI and visibility
issues
This can likely be done safely, but there are definitely some concerns
here with how you want to enable the use of basic block
labels/sections.
-eric
compare bytes. Using function sections can still run into language
specific problems as well, hence why icf=safe vs icf=all. Though pcc's
recent work helps out a little bit via significant address
determination in the front end
- order files and other similar mechanisms work based on externally
visible symbols and are fairly straightforward to use in an ABI safe
way (no function -> function fallthrough for example)
- basic block sections can run into the multiple entry point problems
depending on how they're laid out as well as other ABI and visibility
issues
This can likely be done safely, but there are definitely some concerns
here with how you want to enable the use of basic block
labels/sections.
They're both in the same arena.
>
>>
>> compare bytes. Using function sections can still run into language
>> specific problems as well, hence why icf=safe vs icf=all. Though pcc's
>
>
> Agreed, and I would like to humbly suggest that I added those options to gold originally and am aware of that.
Of course, but context is helpful for discussion and for others in the
future understanding the discussion.
>
>>
>> recent work helps out a little bit via significant address
>> determination in the front end
>> - order files and other similar mechanisms work based on externally
>> visible symbols and are fairly straightforward to use in an ABI safe
>> way (no function -> function fallthrough for example)
>> - basic block sections can run into the multiple entry point problems
>> depending on how they're laid out as well as other ABI and visibility
>> issues
>>
>>
>> This can likely be done safely, but there are definitely some concerns
>> here with how you want to enable the use of basic block
>> labels/sections.
>
>
> I would like to expose basic block sections independently and let me talk more about one more effort we are trying to do. Independent of Propeller, we are looking at a machine learning based approach to do code layout at link time. I won't go into the details of the features but having basic block sections allows us to naturally feed the native object files into a black box that would try several different reorderings of the sections and learn a model. We looked at basic block sections as the building block on top of which Propeller was built. I don't think it would be the only application, just my opinion.
>
Again a question of "why at the object level rather than a low level
IR" seems like something we should really explore the tradeoffs for at
more length.
-eric
Seems like you could spend just as much work and reliability on making
MIR/late stage LTO compilation handle this for you?
-eric
ThanksSri
-eric
> Thanks
> Sri
>
>
>>
>>
>> -eric
>>
>> > Thanks
>> > Sri
>> >
>> >
>> >
>> >
>> >>
>> >>
>> >> -eric
>> >>
>> >>
>> >> > >
>> >> > > In any case, if we're adding flags to clang other than "enable propeller", I think we should have a separate thread on cfe-dev to discuss them. We don't expose every possible LLVM optimization and code generation option through clang. Users have a strong expectation of stability for clang options, and that means we need to evaluate each new option carefully, to decide whether it's something we really want to support indefinitely.
>> >> >
>> >> > Understood.
>> >> >
>> >> > Thanks
>> >> > Sri
>> >> >
>> >> > >
>> >> > > -Eli
>> >> > _______________________________________________
>> >> > LLVM Developers mailing list
>> >> > llvm...@lists.llvm.org
>> >> > https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev
One could imagine using MIR as an "object-file" format from the compiler to the linker -- and then within the linker, doing block layout on the MIR and lowering to actual instructions.
*Pessimization/overhead for stack unwinding used by system-wide profilers and
for exception handling*
Larger CFI programs put an extra burden on unwinding at runtime as more CFI
(and thus native) instructions have to be executed. This will cause more
overhead for any profiler that records stack traces, and, as you correctly note
in the proposal, for any program that heavily uses exceptions.
Thanks for clarifying. This means once you move to the next basic block (or any other basic
block in the function) you have to execute an entirely new set of CFI instructions
except for the common CIE part. While indeed this is not as bad, on average, the overall
active memory footprint will increase.
Creating one FDE per basic block means that .eh_frame_hdr, an allocatable section,
will be bloated too. This will increase the FDE lookup time. I don’t see .eh_frame_hdr
being mentioned in the proposal.
Maksim
_______________________________________________
You’re correct, except that, in Propeller, CFI duplication happens for every basic block as it operates with the conservative assumption that a block can be put anywhere by the linker. That’s a significant bloat that is not cleaned up later.
So, during link time, if N blocks from the same function are contiguous in the final layout, as it should happen most of the time for any sane BB order, we would have several FDEs for a region that only needs one. The bloat goes to the final binary (a lot
more FDEs, specifically, one FDE per basic block).
BOLT will only split a function in two parts, and only if it has profile. Most of the time, a function is not split. It also has an option not to split at all. For internally reordered basic blocks of a given function, it has CFI deduplication logic (it will
interpret and build the CFI states for each block and rewrite the CFIs in a way that uses the minimum number of instructions to encode the states for each block).
TLDR; clang is pathological and the actual CFI bloat will go down from 7x to
2x.
Let us present the CFI Bloats for each benchmark with the default option, which
is creating basic block sections only for functions with samples. For clang,
it is 7x and *not 17x* (the 17 x number is for all sections), and for the
large benchmarks it is less than 30% or 1.3x. For large benchmarks, Storage is
the highest, going from 18M to 23M, ~30%. Clang is almost pathological here.
This is because 10% of all functions in clang have samples (touched by the
profile information), and all of these functions get full basic block sections.
Whereas, for the large benchmarks, only 0.5% of functions have samples. Now,
for clang, 10% of functions that have samples also happen to contain 35% of all
basic blocks. This means, we are creating sections for 35% of all basic blocks
and the CFI bloats are clearly showing.
Now, the data for clang also shows that only 7% of the basic blocks have
samples. We are working on restricting basic block sections to only those basic
blocks that have samples. The rest of the basic blocks (cold) in that function
would share the same section. With this, we would reduce the bloat of CFI
from 7x to 2x. This is not hard to do and we will follow up with this patch.
Also for object size bloats with regards to eh_frame, the reasoning is similar.
Restricting the section creation to only basic blocks that have profiles will
reduce this a lot more.
Importantly, if CFI support were available for discontiguous ranges we wouldn't
have to duplicate CFI FDEs and the bloats would be near minimal.
BOLT parses CFI and DWARF and generates compact information by rewriting it.
Whereas, Propeller uses lld which uses relocations and sections to fixup but
does not rewrite it. This is by design and that lld is not DWARF and CFI
aware. We designed basic block sections just like function sections. The
compiler produces a bunch of sections and relocations. The linker patches the
relocations and the debug info and CFI are right, that's it. For CFI, since
there is no support for discontiguous ranges we have to duplicate and dedup
FDEs only for blocks with sections. We are asking that CFI support
discontiguous ranges and this would look even simpler. Alternately, if lld
were made DWARF and CFI aware we could rewrite it compactly like BOLT.
These would help with object size bloats and binary size bloats.
There are two things that need to be clarified here:
1) Extra CFI FDE entries for basic blocks does not mean more dynamic
instructions are executed. In fact, they do not increase at all. Krys
talked about this earlier.
2) We do deduplication of common static CFI instructions in the FDE
and move it to the CIE . Hence, moving to a new basic block does not
mean a completely new set of CFI instructions is executed.
TLDR; This can be implemented in propeller easily. It was considered
low priority because exception handling code is assumed in general not
performance critical .
Currently, we do not create basic block sections for any block that is
involved in exception handling. There is nothing fundamentally
difficult about exception handling basic blocks. The exception table
uses address offsets which currently does not use relocations, and we
must modify it to use relocations in order to enable sections for
exception handling basic blocks.
We will re-prioritize and send out a patch to handle exception basic blocks.
Thanks
Sri
TLDR; Our relaxation pass is similar to what LLVM’s MCAssembler does
but with a caveat for efficiency. Our experimental results show it is
efficient and convergence is guaranteed.
Our relaxation pass is very similar to what MCAssembler does as it
needs to solve the same problem, which is laying out basic blocks and
optimizing branch instruction sizes based on jump offset. The optimal
algorithm to do this is quadratic and it involves computating of
section offsets multiple times. Our relaxation pass has a caveat for
efficiency. Our relaxation pass recomputes section offsets only after
all the sections have been processed. This is repeated until
convergence, whereas, the MCAssembler re-computes section offsets
every time a section is modified. Our pass works well in the linker as
it minimizes the section offset recomputation, which is more expensive
to do in the linker (whole program) than doing it in the assembler
(per function).
We shrink jump instructions aggressively and then fix the wrongly
shrunk branches by running a pass that grows them back. We guarantee
convergence because we always shrink in the first round and always
grow in the second.
Data shows that our relaxation is efficient. For example, for clang,
the aggressive shrinking affects ~995000 branches in 6 iterations and
only 4 branches had to be grown back.
TLDR; We have provided a Makefile in the git repo, under
llvm-propeller/splo, to compare BOLT and Propeller performance. “make
check-performance” with pointers to the BOLT binaries should do the
comparisons.
In terms of performance optimization for code layout, fundamentally,
BOLT and Propeller are similar. In fact, in one experiment, we
extracted the basic block order from BOLT and just used basic block
sections and linker symbol ordering to perform the same order during
linking. The performance was identical and the ordering was exactly
as expected.
Some recent changes in Propeller has caused it to regress by a small
amount and is sometimes getting hidden by the small noise. We are
debugging the performance issue and it looks like an alignment issue
or a second order effect. Currently, because of this bug, BOLT is
faster than Propeller by about 0.5% to 1% on Clang benchmark.
Also, we have been addressing each topic piecemeal to keep discussions
focussed. I will provide a consolidated document putting together our
clarifications on all the concerns raised.
Large binaries are built using debug fission to as it significantly
improves the scalability of debug builds,
https://gcc.gnu.org/wiki/DebugFission. Quoting from the page which
again reiterates the importance of scalability and incremental builds,
Fission was invented to mitigate the following issues for debug builds
of large applications:
* Out of memory errors
* Slow link times
* Slow gdb startup times
Debug Fission essentially splits the dwarf info into a separate object
file and the link action memory overheads are smaller as the input
object files and the resulting binary are smaller.
Propeller supports both fission and non-fission builds. Even with
fission, the dwarf objects are not relocatable, so the extra
relocations needed by Propeller would still go into the main object
file that contains code. Also, Propeller uses extra relocations for
location lists too.
We looked at BOLT and it does not seem to have any support for
binaries built with fission, something we missed mentioning in the
original RFC. Fission supports a mode which allows the individual
dwarf objects, dwo files, to be linked into a single dwp file and
BOLT would have to also rewrite the fission object file and likely in
the same process.
Another concern raised was regarding debugging overhead. The specific
concern here is that having more debug ranges with basic block
sections might increase overheads of debugging tools. We have not
evaluated this, any benchmarks that could measure these overheads?
Also, this can be mitigated by just exposing debug_ranges to lld. The
various ranges created for each basic block are contiguous as most
basic blocks end up together and these ranges can be collapsed by the
linker into a larger range.
Is there large value from deferring the block ordering to link time? That is, does the block layout algorithm need to consider global layout issues when deciding which blocks to put together and which to relegate to the far-away part of the code?Or, could the propellor-optimized compile step instead split each function into only 2 pieces -- one containing an "optimally-ordered" set of hot blocks from the function, and another containing the cold blocks? The linker would have less flexibility in placement, but maybe it doesn't actually need that flexibility?Apologies if this is obvious for those who actually know what they're talking about here. :)
Hi Sri,
I want to clarify one thing before sending a detailed reply: did you evaluate
BOLT on Clang built with basic block sections? In the makefile you reference,
there are two versions: a “vanilla” and a default built with function sections.
High overheads you see with BOLT on Clang do not match our experience.
Thanks,
Maksim
Hi Sri,
I want to clarify one thing before sending a detailed reply: did you evaluate
BOLT on Clang built with basic block sections?
In the makefile you reference,
there are two versions: a “vanilla” and a default built with function sections.
High overheads you see with BOLT on Clang do not match our experience.
Cool. The new numbers look good. If you run BOLT with jemalloc library
preloaded, you will likely get a runtime closer to 1 minute. We’ve noticed that
compared to the default malloc, it improves the multithreaded
performance and brings down memory usage significantly.
Thanks,
Maksim
Cool. The new numbers look good. If you run BOLT with jemalloc library
preloaded, you will likely get a runtime closer to 1 minute. We’ve noticed that
compared to the default malloc, it improves the multithreaded
performance and brings down memory usage significantly.
Hi Sri,
Thank you for replying to our feedback. 7 out 12 high-level concerns have been
answered; 2 of them are fully addressed. The rest are being tracked at the
following Google doc:
https://docs.google.com/document/d/1jqjUZc8sEoNl6_lrVD6ZkITyFBFqhUOZ6ZaDm_XVbb8/
To keep the discussion at a high level, I have to reference the LTO vs ThinLTO
comparison since that appears to be the central theme in your response to the
feedback.
Unlike LTO, BOLT does not have to keep the entire program in memory.
Furthermore, as we have previously mentioned, most of the passes are run in
parallel, and the performance scales well with the number of CPUs.
To demonstrate that running BOLT on a hot subset of functions is not just a
speculation, we have prototyped a "Thin" version that optimizes Clang-7 in under
15 seconds using less than 4GB of memory. No modifications to the linker or
compiler were required. And by the way, that appears to be faster than just the
re-linking phase of the Propeller. Larger loads show similar improvements
providing 2x-5x savings over the original processing specs.
Let me reiterate that current BOLT requires large amounts of memory not because
it's a fundamental limitation, unlike LTO. For us, system memory was never a
constraint. The runtime of the application, not BOLT, was the primary goal
during the development.
ThinLTO design solves a real problem and dramatically improves compilation time
even when building on a single node. ThinLTO results provide "end-to-end build
time" comparison to LTO. I've asked you to show a similar comparison for
Propeller vs. BOLT. I haven't seen the results, and I suspect the total overhead
will exceed that of even the oldest non-parallel version of BOLT.
One argument I've heard is that BOLT is not taking advantage of the distributed
build system. That's correct. It does not have to since it does not require to
rebuild the application. In "Thin" mode, the overhead is similar to a regular
linker running with a linker script.
You are right that we do not support debug fission packages. It is unimplemented
for a simple reason: no one asked for it previously. And as we like to say in
the open-source community: "patches are welcome."
Maksim
P.S. We have updated https://github.com/facebookincubator/BOLT with instructions on running BOLT with jemalloc or tcmalloc.
Hi Sri,
Thank you for replying to our feedback. 7 out 12 high-level concerns have been
answered; 2 of them are fully addressed. The rest are being tracked at the
following Google doc:
https://docs.google.com/document/d/1jqjUZc8sEoNl6_lrVD6ZkITyFBFqhUOZ6ZaDm_XVbb8/
To keep the discussion at a high level, I have to reference the LTO vs ThinLTO
comparison since that appears to be the central theme in your response to the
feedback.
Unlike LTO, BOLT does not have to keep the entire program in memory.
Furthermore, as we have previously mentioned, most of the passes are run in
parallel, and the performance scales well with the number of CPUs.
To demonstrate that running BOLT on a hot subset of functions is not just a
speculation, we have prototyped a "Thin" version that optimizes Clang-7 in under
15 seconds using less than 4GB of memory. No modifications to the linker or
compiler were required. And by the way, that appears to be faster than just the
re-linking phase of the Propeller. Larger loads show similar improvements
providing 2x-5x savings over the original processing specs.
Let me reiterate that current BOLT requires large amounts of memory not because
it's a fundamental limitation, unlike LTO. For us, system memory was never a
constraint. The runtime of the application, not BOLT, was the primary goal
during the development.
ThinLTO design solves a real problem and dramatically improves compilation time
even when building on a single node. ThinLTO results provide "end-to-end build
time" comparison to LTO. I've asked you to show a similar comparison for
Propeller vs. BOLT. I haven't seen the results, and I suspect the total overhead
will exceed that of even the oldest non-parallel version of BOLT.
One argument I've heard is that BOLT is not taking advantage of the distributed
build system. That's correct. It does not have to since it does not require to
rebuild the application. In "Thin" mode, the overhead is similar to a regular
linker running with a linker script.
1) Exception Handling
TLDR; We now handle exceptions and can optimize exception heavy
benchmarks and clang with exceptions enabled.
We have updated Propeller patches to handle exceptions. In contrast to
the previous approach which grouped all exception-handling related
basic blocks into one section, we now group just the landing pads
within one section and create a separate section for every other basic
block (even those which potentially throw exceptions by calling other
functions). This allows us to reorder code for exception-heavy
programs with greater flexibility. Grouping all landing pad basic
blocks in the same section lets us satisfy the ABI requirement of “one
landing pad fragment per procedure fragment.” This could be further
relaxed to separately grouping together all the landing pads which are
used by every basic block section. We will leave this for future
updates.
With this change, we are able to speedup the SPEC benchmark xalancbmk
by 0.5% which we were unable to do so previously. (Without exception
handling support this benchmark regressed by 1%).
We also built clang with exceptions enabled (DLLVM_ENABLE_EH=ON) and
were able to get performance improvements similar to what we got
without exceptions.
2) Minimizing the number of basic block sections
TLDR; We have significantly cut down the number of basic blocks for
which sections are created.
Previously, basic block sections were created for all the basic
blocks in a function that had samples. We have modified the patches
to allow creation of sections for only those basic blocks that have
samples. This greatly reduces the number of sections created. This
does not affect benchmark performance in any manner. For clang,
making this change reduces the object size bloat from 100% (2x) to 50%
(1.5x). Clang is an extreme benchmark for object size bloats since the
perf samples touch 10% of all functions. For larger datacenter
workload benchmarks we evaluated, the object size bloats are less than
5% with this change.
We are working on further reducing this so that the object file size
bloats can be kept as minimal as possible. Currently, we create a
section for a basic block even if it received just one perf sample.
This is overkill and can be restricted to only basic blocks that were
really hot.
3) Interprocedural Basic Block Reordering
TLDR; Interprocedural basic block reordering further improves
performance by 1% (clang benchmark) but is slower and we are working
on a better implementation.
Previously, the function reordering done was intra-procedural and one
of the comments was that if this could be done inter-procedurally. We
have now added an option to do this inter-procedurally, it is not the
default. While the inter-procedural reordering algorithm is slower by
10x (increasing from 3 seconds to 30 seconds for clang) because the
code layout search space is much larger, the performance of the final
benchmark improves by an additional 1%. We are working on improving
the implementation to make this much faster.
4) Propeller functionality in lld is separated out
After discussing with Rui, we have moved Propeller code into a
separate directory. Propeller interfaces with lld through
well-defined API. This provides the advantage that Propeller code
itself and future changes can be reviewed independently.
5) Reusing optimized LLVM IR files to re-link
TLDR; We are working on re-using native object files from previous
link and patches will be available soon
The final re-link with Propeller can directly start from the high
level optimized IR files. We are working on a patch to support
options “--save-precodegen” which will automatically save optimized IR
files which will later be reused by Propeller to reduce link time,
Branch : https://github.com/google/llvm-propeller/tree/plo-codegen-backend.
We are also working on directly using native object files from the
previous link for those objects which did not accrue any profiling
samples. Even for the clang benchmark, only 7% of the objects needed
to be re-generated with basic block sections and the cached native
object files can be used for the rest. This approach works
particularly well with ThinLTO as the lto.cache can directly be used
to obtain the native object files for modules that are not affected.
We will send out patches for this soon.
6) Time Overheads of CFI and Debug Info
TLDR; Experiments show varying overheads with Debug Info depending
on the symbolizers. We did not see any overheads with CFI.
Collapsing .debug_ranges in the linker solves the overheads associated
with this.
We tried to measure two things. Overheads from accessing .eh_frame
and .debug_ranges. With Basic block sections, a new CFI FDE is
created for every basic block that gets a new section and a new range
pair is added to debug_ranges. We wrote a benchmark , using
libunwind, to dump the stack trace (no symbolization) and then do then
dump the stack trace running the symbolizer (symbolization) using a
Google Internal API.
The benchmark had ~12000 functions and basic block sections for all
basic blocks resulted in ~130000 more basic block sections and
equivalent debug range entries, an order of magnitude more.
For a vanilla binary, on average over several iterations, it took
~70ns to dump the stack trace without symbolization and ~235000 ns
with symbolization. We then enabled basic block sections across the
board and re-measured and found no measurable impact. Symbolization
seems to dominate the time.
We then measured overheads with addr2line and llvm-symbolizer. We
just timed symbolizing a couple of addresses. With addr2line, the
binary with sections slows down by 5% to 10%. With llvm-symbolizer,
the slowdown is much more, by upto 30% sometimes. Looking at the
symbolizer code, we found places where the symbols are scanned
linearly and also places where the .debug_ranges are not collapsed.
Looking at the different symbolizer implementations, we found that
some use binary search, some collapse debug ranges as and when they
are added and some just do a linear search.
The primary reason for this overhead is that the .debug_ranges are
much larger. We are looking at improving this by doing one of these:
a) Have the linker collapse the debug_ranges which are pretty much
contiguous as most of the basic blocks end up in the same function.
This is the easier solution and we are working on a patch. This should
completely solve the problem.
b) Alternately, modify the symbolizers to collapse the ranges as and
when they are added.
All our experiments were with full basic block sections. When basic
block sections are turned on selectively, these problems should not
manifest even now.
7) New Relocation types to simplify linker relaxation of Basic Block
Section Jumps
We are working on adding two new relocation types: R_X86_64_PC32_JUMPX
and R_X86_64_PC8_JUMPX to the x86-64 ps-ABI to make linker handing of
relaxation simplified. Specifically, these two relocation types will
allow the linker to easily identify the jump instructions that were
created for bb sections. The PC8 relocations will only have to be
checked for growing to 32 bits during relaxation.
Proposal here: https://groups.google.com/forum/?hl=en#!topic/x86-64-abi/-2AkYw1QJuI
Thanks,
Sri
I took some time to understand the lld side changes
(D68065+D73497+D68073; the patch do not have clear relations and do not
apply at HEAD cleanly), attended a LLVM Social yesterday and asked some
people about their feelings. We are still dubious whether doing all the
heavy lifting on the linker side is the right approach. Disassembly at
linker side may give some short term benefits, but the interaction with
debug info/asynchronous unwind tables/symbol information is muddy. (From
the existing posted patch series it is not clear how they are solved.)
Doing non-LTO things on the linkder side is rather inflexible.
Linkers don't really understand the semantics. So I see things like:
* Get the input section size, check if there is a relocation (for JMP)
r_offset = input_section_size-4, if there is a JCC at r_offset =
input_section_size-9, ...
* Update st_value to take shrunk input sections into account.
* An algorithm to the extended travelling salesman problem looks fancy.
It also introduced a bunch of parameters. Have other ordering approaches been considered?
I am concerned that the raw ELF fields (like sh_size) are used for
heuristics. It seems to try reconstructing some information about
Machine{Function,BasicBlock,Instr,...} about doing that in the
Machine* stage may just be simpler and more accurate.
We feel that the available optimizations may be limited. The lld focused
implementation may have some improvement in the near term but the
benefits will dilute quickly when the Machine{Function,BasicBlock}
interfaces get improved.
Also, doing heavy-lifting work on the linker side:
Needs non-trivial work to port to another binary format. Another target
may be a very different story. (Even for x86-64, I see the patches
attend to address R_X86_64_PC8/R_X86_64_PC32/R_X86_64_PLT32 calls, but
the two relocation types are not handled.)
The newly lld/ELF code overlaps with jobs of MachineBlockPlacement/BranchFolding/AsmPrinter/etc.
If the current MachineFunction interface is not flexible (e.g. it does
not allow sharable MachineBasicBlocks between MachineFunctions.) enough,
let's fix it.
* An algorithm to the extended travelling salesman problem looks fancy.
It also introduced a bunch of parameters. Have other ordering approaches been considered?
I am concerned that the raw ELF fields (like sh_size) are used for
heuristics. It seems to try reconstructing some information about
Machine{Function,BasicBlock,Instr,...} about doing that in the
Machine* stage may just be simpler and more accurate.
I will specifically answer the relocations part as Rahman has
addressed the other things. This was discussed previously and Eli and
Rui suggested we use specific relocations for jmp instructions which
can be relaxed. We are working on that one here:
https://groups.google.com/g/x86-64-abi/c/-2AkYw1QJuI With the
relocations added, the relaxation code would become a lot simpler.