diff --git a/CMakeLists.txt b/CMakeLists.txt index 75042cfa8fc..2ece1b802ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -291,7 +291,22 @@ if(CONFIG_ESP_SYSTEM_USE_FRAME_POINTER) endif() endif() -list(APPEND link_options "-fno-lto") +if(CONFIG_COMPILER_LTO_LINKTIME AND NOT BOOTLOADER_BUILD AND NOT ESP_TEE_BUILD) + list(APPEND link_options "-flto=auto") + + if(CONFIG_APP_REPRODUCIBLE_BUILD) + # LTO generates code at link time, where the path remapping applied to + # compile_options doesn't take effect, so pass it to the linker as well. + # -save-temps keeps LTRANS objects out of $TMPDIR, and a pinned random + # seed makes LTO bytecode byte-identical. See the commit message for + # details. + list(APPEND link_options ${prefix_map_compile_options}) + list(APPEND link_options "-save-temps") + list(APPEND compile_options "-frandom-seed=1") + endif() +else() + list(APPEND compile_options "-fno-lto") +endif() if(CONFIG_IDF_TARGET_LINUX AND CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") # Not all versions of the MacOS linker support the -warn_commons flag. @@ -383,6 +398,27 @@ foreach(component_target ${build_component_targets}) set(__idf_component_context 0) endforeach() +if(CONFIG_COMPILER_LTO_COMPILETIME AND NOT BOOTLOADER_BUILD AND NOT ESP_TEE_BUILD) + include("${CMAKE_CURRENT_LIST_DIR}/tools/cmake/lto.cmake") + idf_build_get_property(build_components BUILD_COMPONENTS) + # Components whose object code is placed by a linker fragment (their own or + # another component's) must not be compiled with LTO, otherwise the placement + # stops matching. See tools/cmake/lto.cmake for details. + __lto_collect_fragment_placed_components(lto_placed_components ${build_components}) + # For each component, enable LTO unless it has its own linker fragments, is + # placed by some fragment, has opted out via NO_LTO, or is not a static library. + foreach(component_name ${build_components}) + idf_component_get_property(ldfragment ${component_name} LDFRAGMENTS) + idf_component_get_property(no_lto ${component_name} NO_LTO) + idf_component_get_property(component_lib ${component_name} COMPONENT_LIB) + get_target_property(type ${component_lib} TYPE) + if(NOT ldfragment AND NOT no_lto AND NOT component_name IN_LIST lto_placed_components + AND type STREQUAL "STATIC_LIBRARY") + target_compile_options(${component_lib} PRIVATE -flto=auto) + endif() + endforeach() +endif() + # Run component validation checks after all components have been processed # Only run validation for the main project, not subprojects like bootloader idf_build_get_property(bootloader_build BOOTLOADER_BUILD) diff --git a/Kconfig b/Kconfig index 064e4091115..1a70c322166 100644 --- a/Kconfig +++ b/Kconfig @@ -787,6 +787,47 @@ mainmenu "Espressif IoT Development Framework Configuration" default "gcc" if COMPILER_RT_LIB_GCCLIB default "" if COMPILER_RT_LIB_HOST + config COMPILER_LTO_LINKTIME + bool "Enable LTO at link time" + default n + # LTO for host builds is probably not needed at all + # LTO with Clang needs LLD (IDF-8286) + # LTO works with APP_REPRODUCIBLE_BUILD (see top-level CMakeLists.txt) + depends on !IDF_TARGET_LINUX && !IDF_TOOLCHAIN_CLANG + help + If enabled, ESP-IDF build system will tell the linker to use LTO + (Link Time Optimization). This option may increase linking time but + will also produce smaller binaries. + Note that to see some code size reduction, you should also enable LTO + at compile time for some components. + + You can do this either selectively for specific components, or use + the option CONFIG_COMPILER_LTO_COMPILETIME below to enable + it for most of the components by default. + + Note that LTO can also increase task stack usage, because inlining + across translation units produces fewer but larger stack frames. After + enabling LTO, check the stack high water mark of your tasks and raise the + stack sizes of the affected tasks if necessary. + + config COMPILER_LTO_COMPILETIME + bool "Enable LTO by default at compile time" + depends on COMPILER_LTO_LINKTIME + default n + help + If enabled, ESP-IDF build system will compile most of the components with + LTO (link time optimizations) enabled. + + Components which have link-time sections placement requirements will not + have LTO enabled. This includes components which specify linker fragment + files, as well as components whose object code is placed by a linker + fragment of another component. + + Note that enabling LTO for all components does not always reduce the + binary size, and in some cases can increase it. For a more predictable + result, consider enabling LTO selectively for specific components by + adding the -flto=auto compile option to them. + choice COMPILER_ORPHAN_SECTIONS prompt "Orphan sections handling" default COMPILER_ORPHAN_SECTIONS_ERROR diff --git a/tools/cmake/lto.cmake b/tools/cmake/lto.cmake new file mode 100644 index 00000000000..3c31ebc1b52 --- /dev/null +++ b/tools/cmake/lto.cmake @@ -0,0 +1,50 @@ +# Helpers for link-time optimization (LTO) support. +# +# These helpers are shared between the legacy build system (tools/cmake) and the +# build system v2 (tools/cmakev2). They only rely on the component property API +# (idf_component_get_property) and the COMPONENT_DIR / LDFRAGMENTS component +# properties, which are available in both build systems. + +include_guard(GLOBAL) + +# __lto_collect_fragment_placed_components( ...) +# +# Returns, in output_var, the subset of the given components whose object code is +# placed by an explicit "archive: libNAME.a" directive in some linker fragment +# (.lf) file - whether that fragment belongs to the component itself or to a +# different component. +# +# Such components rely on section placement that is matched by object file / +# archive name. LTO merges and renames the object files of a component, so the +# placement rules silently stop matching and the affected functions (for example +# the spi_flash / GDMA HAL routines that must execute from IRAM while the flash +# cache is disabled) end up in flash. Compiling these components without LTO keeps +# their placement intact. +# +# Wildcard directives ("archive: *") are intentionally ignored: they match by +# section name rather than by object file name and are therefore not affected by +# LTO. +function(__lto_collect_fragment_placed_components output_var) + set(placed "") + foreach(component_name ${ARGN}) + idf_component_get_property(component_dir ${component_name} COMPONENT_DIR) + idf_component_get_property(ldfragments ${component_name} LDFRAGMENTS) + foreach(ldfragment ${ldfragments}) + get_filename_component(ldfragment_abs "${ldfragment}" ABSOLUTE BASE_DIR "${component_dir}") + if(EXISTS "${ldfragment_abs}") + file(READ "${ldfragment_abs}" ldfragment_contents) + string(REGEX MATCHALL "archive:[ \t]*lib[A-Za-z0-9_-]+\\.a" + archive_matches "${ldfragment_contents}") + foreach(archive_match ${archive_matches}) + string(REGEX REPLACE "archive:[ \t]*lib([A-Za-z0-9_-]+)\\.a" "\\1" + archive_name "${archive_match}") + list(APPEND placed ${archive_name}) + endforeach() + endif() + endforeach() + endforeach() + if(placed) + list(REMOVE_DUPLICATES placed) + endif() + set(${output_var} "${placed}" PARENT_SCOPE) +endfunction() diff --git a/tools/cmake/toolchain.cmake b/tools/cmake/toolchain.cmake index 75f1a2a28a6..7ab605cd856 100644 --- a/tools/cmake/toolchain.cmake +++ b/tools/cmake/toolchain.cmake @@ -43,6 +43,12 @@ else() set(CMAKE_C_COMPILER ${_CMAKE_TOOLCHAIN_PREFIX}gcc) set(CMAKE_CXX_COMPILER ${_CMAKE_TOOLCHAIN_PREFIX}g++) set(CMAKE_ASM_COMPILER ${_CMAKE_TOOLCHAIN_PREFIX}gcc) + # Use the gcc-ar/gcc-ranlib wrappers so that the LTO plugin is loaded when + # creating and indexing static archives. This is required for link-time + # optimization (CONFIG_COMPILER_LTO_LINKTIME) to work across static libraries; + # plain ar/ranlib do not record the LTO symbols in the archive index. + set(CMAKE_AR ${_CMAKE_TOOLCHAIN_PREFIX}gcc-ar) + set(CMAKE_RANLIB ${_CMAKE_TOOLCHAIN_PREFIX}gcc-ranlib) endif() # Handle different execution contexts for the toolchain file.