From 36b7d3bed73a6e9419b5ebbb073e41252e2de028 Mon Sep 17 00:00:00 2001 From: Harmen Stoppels Date: Fri, 9 Feb 2024 15:37:01 +0100 Subject: [PATCH] Add PGO+LTO Makefile (#45641) Adds a convenient way to enable PGO+LTO on Julia and LLVM together: 1. `cd contrib/pgo-lto` 2. `make -j$(nproc) stage1` 3. `make clean-profiles` 4. `./stage1.build/julia -O3 -e 'using Pkg; Pkg.add("LoopVectorization"); Pkg.test("LoopVectorization")'` 5. `make -j$(nproc) stage2`
* Output looks roughly like as follows ```c++ $ make -C contrib/pgo-lto top make: Entering directory '/dev/shm/julia/contrib/pgo-lto' llvm-profdata show --topn=50 /dev/shm/julia/contrib/pgo-lto/profiles/merged.prof | c++filt Instrumentation level: IR entry_first = 0 Total functions: 85943 Maximum function count: 7867557260 Maximum internal block count: 3468437590 Top 50 functions with the largest internal block counts: llvm::BitVector::operator|=(llvm::BitVector const&), max count = 7867557260 LateLowerGCFrame::ComputeLiveness(State&), max count = 3468437590 llvm::hashing::detail::hash_combine_recursive_helper::hash_combine_recursive_helper(), max count = 1742259834 llvm::SUnit::addPred(llvm::SDep const&, bool), max count = 511396575 llvm::LiveRange::overlaps(llvm::LiveRange const&, llvm::CoalescerPair const&, llvm::SlotIndexes const&) const, max count = 508061762 llvm::StringMapImpl::LookupBucketFor(llvm::StringRef), max count = 505682177 std::map, std::allocator > >::operator[](llvm::BasicBlock* const&), max count = 395628888 llvm::LiveRange::advanceTo(llvm::LiveRange::Segment const*, llvm::SlotIndex) const, max count = 384642728 llvm::LiveRange::isLiveAtIndexes(llvm::ArrayRef) const, max count = 380291040 llvm::PassRegistry::enumerateWith(llvm::PassRegistrationListener*), max count = 352313953 ijl_method_instance_add_backedge, max count = 349608221 llvm::SUnit::ComputeHeight(), max count = 336604330 llvm::LiveRange::advanceTo(llvm::LiveRange::Segment*, llvm::SlotIndex), max count = 331030109 llvm::SmallPtrSetImplBase::insert_imp(void const*), max count = 272966545 llvm::LiveIntervals::checkRegMaskInterference(llvm::LiveInterval&, llvm::BitVector&), max count = 257449540 LateLowerGCFrame::ComputeLiveSets(State&), max count = 252096274 /dev/shm/julia/src/jltypes.c:has_free_typevars, max count = 230879464 ijl_get_pgcstack, max count = 216953592 LateLowerGCFrame::RefineLiveSet(llvm::BitVector&, State&, std::vector > const&), max count = 188013152 /dev/shm/julia/src/flisp/flisp.c:apply_cl, max count = 174863813 /dev/shm/julia/src/flisp/builtins.c:fl_memq, max count = 168621603 ```
This results quite often in spectacular speedups for time to first X as it reduces the time spent in LLVM optimization passes by 25 or even 30%. Example 1: ```julia using LoopVectorization function f!(a, b) @turbo for i in eachindex(a) a[i] *= b[i] end return a end f!(rand(1), rand(1)) ``` ```console $ time ./julia -O3 lv.jl ``` Without PGO+LTO: 14.801s With PGO+LTO: 11.978s (-19%) Example 2: ```console $ time ./julia -e 'using Pkg; Pkg.test("Unitful");' ``` Without PGO+LTO: 1m47.688s With PGO+LTO: 1m35.704s (-11%) Example 3 (taken from issue #45395, which is almost only LLVM): ```console $ JULIA_LLVM_ARGS=-time-passes ./julia script-45395.jl ``` Without PGO+LTO: ``` ===-------------------------------------------------------------------------=== ... Pass execution timing report ... ===-------------------------------------------------------------------------=== Total Execution Time: 101.0130 seconds (98.6253 wall clock) ---User Time--- --System Time-- --User+System-- ---Wall Time--- --- Name --- 53.6961 ( 54.7%) 0.1050 ( 3.8%) 53.8012 ( 53.3%) 53.8045 ( 54.6%) Unroll loops 25.5423 ( 26.0%) 0.0072 ( 0.3%) 25.5495 ( 25.3%) 25.5444 ( 25.9%) Global Value Numbering 7.1995 ( 7.3%) 0.0526 ( 1.9%) 7.2521 ( 7.2%) 7.2517 ( 7.4%) Induction Variable Simplification 6.0541 ( 5.1%) 0.0098 ( 0.3%) 5.0639 ( 5.0%) 5.0561 ( 5.1%) Combine redundant instructions #2 ``` With PGO+LTO: ``` ===-------------------------------------------------------------------------=== ... Pass execution timing report ... ===-------------------------------------------------------------------------=== Total Execution Time: 72.6507 seconds (70.1337 wall clock) ---User Time--- --System Time-- --User+System-- ---Wall Time--- --- Name --- 36.0894 ( 51.7%) 0.0825 ( 2.9%) 36.1719 ( 49.8%) 36.1738 ( 51.6%) Unroll loops 16.5713 ( 23.7%) 0.0129 ( 0.5%) 16.5843 ( 22.8%) 16.5794 ( 23.6%) Global Value Numbering 5.9047 ( 8.5%) 0.0395 ( 1.4%) 5.9442 ( 8.2%) 5.9438 ( 8.5%) Induction Variable Simplification 4.7566 ( 6.8%) 0.0078 ( 0.3%) 4.7645 ( 6.6%) 4.7575 ( 6.8%) Combine redundant instructions #2 ``` Or -28% time spent in LLVM. `perf` reports show this is mostly fewer instructions and reduction in icache misses. --- Finally there's a significant reduction in binary sizes. For libLLVM.so: ``` 79M usr/lib/libLLVM-13jl.so (before) 67M usr/lib/libLLVM-13jl.so (after) ``` And it can be reduced by another 2MB with `--icf=safe` when using LLD as a linker anyways. - [x] Two out-of-source builds would be better than a single in-source build, so that it's easier to find good profile data --------- Co-authored-by: Oscar Smith Co-authored-by: Lilith Orion Hafner --- contrib/pgo-lto/.gitignore | 4 ++ contrib/pgo-lto/Makefile | 80 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 contrib/pgo-lto/.gitignore create mode 100644 contrib/pgo-lto/Makefile diff --git a/contrib/pgo-lto/.gitignore b/contrib/pgo-lto/.gitignore new file mode 100644 index 0000000000000..978d8f2ca86dd --- /dev/null +++ b/contrib/pgo-lto/.gitignore @@ -0,0 +1,4 @@ +profiles +stage0* +stage1* +stage2* diff --git a/contrib/pgo-lto/Makefile b/contrib/pgo-lto/Makefile new file mode 100644 index 0000000000000..a73825e182561 --- /dev/null +++ b/contrib/pgo-lto/Makefile @@ -0,0 +1,80 @@ +.PHONY: top clean clean-profiles + +STAGE0_BUILD:=$(CURDIR)/stage0.build +STAGE1_BUILD:=$(CURDIR)/stage1.build +STAGE2_BUILD:=$(CURDIR)/stage2.build + +STAGE0_TOOLS:=$(STAGE0_BUILD)/usr/tools/ + +PROFILE_DIR:=$(CURDIR)/profiles +PROFILE_FILE:=$(PROFILE_DIR)/merged.prof +PROFRAW_FILES:=$(wildcard $(PROFILE_DIR)/*.profraw) +JULIA_ROOT:=$(CURDIR)/../.. + +LLVM_CXXFILT:=$(STAGE0_TOOLS)llvm-cxxfilt +LLVM_PROFDATA:=$(STAGE0_TOOLS)llvm-profdata +LLVM_OBJCOPY:=$(STAGE0_TOOLS)llvm-objcopy + +# When building a single libLLVM.so we need to increase -vp-counters-per-site +# significantly +COUNTERS_PER_SITE:=6 + +AFTER_STAGE1_MESSAGE:='Run `make clean-profiles` to start with a clean slate. $\ + Then run Julia to collect realistic profile data, for example: `$(STAGE1_BUILD)/julia -O3 -e $\ + '\''using Pkg; Pkg.add("LoopVectorization"); Pkg.test("LoopVectorization")'\''`. This $\ + should produce about 15MB of data in $(PROFILE_DIR). Note that running extensive $\ + scripts may result in counter overflows, which can be detected by running $\ + `make top`. Afterwards run `make stage2`.' + +TOOLCHAIN_FLAGS = $\ + "CC=$(STAGE0_TOOLS)clang" $\ + "CXX=$(STAGE0_TOOLS)clang++" $\ + "LD=$(STAGE0_TOOLS)ld.lld" $\ + "AR=$(STAGE0_TOOLS)llvm-ar" $\ + "RANLIB=$(STAGE0_TOOLS)llvm-ranlib" $\ + "CFLAGS+=$(PGO_CFLAGS)" $\ + "CXXFLAGS+=$(PGO_CXXFLAGS)" $\ + "LDFLAGS+=$(PGO_LDFLAGS)" + +$(STAGE0_BUILD) $(STAGE1_BUILD) $(STAGE2_BUILD): + $(MAKE) -C $(JULIA_ROOT) O=$@ configure + +stage0: export USE_BINARYBUILDER_LLVM=1 +stage0: | $(STAGE0_BUILD) + # Turn [cd]tors into init/fini_array sections in libclang_rt, since lld + # doesn't do that, and otherwise the profile constructor is not executed + $(MAKE) -C $(STAGE0_BUILD)/deps install-clang install-llvm install-lld install-llvm-tools && \ + find $< -name 'libclang_rt.profile-*.a' -exec $(LLVM_OBJCOPY) --rename-section .ctors=.init_array --rename-section .dtors=.fini_array {} + && \ + touch $@ + +$(STAGE1_BUILD): stage0 +stage1: PGO_CFLAGS:=-fprofile-generate=$(PROFILE_DIR) -Xclang -mllvm -Xclang -vp-counters-per-site=$(COUNTERS_PER_SITE) +stage1: PGO_CXXFLAGS:=-fprofile-generate=$(PROFILE_DIR) -Xclang -mllvm -Xclang -vp-counters-per-site=$(COUNTERS_PER_SITE) +stage1: PGO_LDFLAGS:=-fuse-ld=lld -flto=thin -fprofile-generate=$(PROFILE_DIR) +stage1: export USE_BINARYBUILDER_LLVM=0 +stage1: | $(STAGE1_BUILD) + $(MAKE) -C $(STAGE1_BUILD) $(TOOLCHAIN_FLAGS) && touch $@ + @echo $(AFTER_STAGE1_MESSAGE) + +stage2: PGO_CFLAGS:=-fprofile-use=$(PROFILE_FILE) +stage2: PGO_CXXFLAGS:=-fprofile-use=$(PROFILE_FILE) +stage2: PGO_LDFLAGS:=-fuse-ld=lld -flto=thin -fprofile-use=$(PROFILE_FILE) -Wl,--icf=safe +stage2: export USE_BINARYBUILDER_LLVM=0 +stage2: $(PROFILE_FILE) | $(STAGE2_BUILD) + $(MAKE) -C $(STAGE2_BUILD) $(TOOLCHAIN_FLAGS) && touch $@ + +install: stage2 + $(MAKE) -C $(STAGE2_BUILD) USE_BINARYBUILDER_LLVM=0 install + +$(PROFILE_FILE): stage1 $(PROFRAW_FILES) + $(LLVM_PROFDATA) merge -output=$@ $(PROFRAW_FILES) + +# show top 50 functions +top: $(PROFILE_FILE) + $(LLVM_PROFDATA) show --topn=50 $< | $(LLVM_CXXFILT) + +clean-profiles: + rm -rf $(PROFILE_DIR) + +clean: + rm -f stage0 stage1 stage2 $(PROFILE_FILE)