Files
esp-idf/tools/cmake/lto.cmake
Ivan Grokhotkov (bot) d20a986999 feat(build): add options to enable link-time optimization (LTO)
Link-time optimization (LTO) lets the compiler inline and optimize across
translation units. ESP-IDF relies heavily on linker-script placement rules
that match object files by name, which LTO does not preserve, so LTO cannot
be enabled for the whole framework. This change adds two opt-in options that
side-step that conflict:

- CONFIG_COMPILER_LTO_LINKTIME tells the linker to perform LTO on any object
  files that carry LTO information (compiled with -flto). On its own this is
  safe: users can add -flto to specific components (e.g. their own libraries)
  to shrink them, without affecting components that use linker fragments.

- CONFIG_COMPILER_LTO_COMPILETIME automatically compiles most
  components with -flto. A component is excluded when it has its own linker
  fragments, when it opts out via the NO_LTO component property, or when its
  object code is placed by *another* component's linker fragment (matched by
  archive name). The last case is handled by tools/cmake/lto.cmake, which
  scans linker fragments for explicit "archive: libNAME.a" placement and
  excludes those components. Without it, functions that must run from IRAM
  while the flash cache is disabled (e.g. the spi_flash / GDMA HAL routines,
  placed in IRAM by spi_flash/esp_driver_dma fragments) would be moved to
  flash by LTO and the device would panic with a cache error at run time.

Both options are disabled for the bootloader and ESP-TEE builds, which depend
on object-file-name based placement. LTO is also gated off for Clang
(needs LLD, IDF-8286) and host builds.

LTO works together with CONFIG_APP_REPRODUCIBLE_BUILD, but needs extra
flags: LTO defers code generation and most debug-info emission from compile
time to link time, where the reproducible-build path remapping (applied to
compile_options only) does not take effect. When both options are enabled,
three extra flags keep the .elf, .bin and .map byte-identical across build
directories (verified on esp32 / GCC 16.1):

- the -f*-prefix-map options are passed to the linker as well, so the LTO
  code generator remaps DW_AT_comp_dir (otherwise the build dir leaks into
  .debug_str, and cascades into esp_app_desc_t.app_elf_sha256 in the .bin);
- -save-temps makes lto-wrapper use stable LTRANS object names in the build
  dir instead of random $TMPDIR paths that leak into the .map;
- -frandom-seed=1 makes LTO GIMPLE bytecode objects byte-identical (a
  shared seed was verified not to collide, including for C++ file-local
  static variables and anonymous namespaces promoted by LTO).

The gcc-ar / gcc-ranlib wrappers are selected in the GCC toolchain file so
that the LTO plugin is loaded when creating and indexing static archives;
plain ar/ranlib do not record LTO symbols in the archive index.

Note: LTO, like other inlining, can also increase binary size. Enable it
together with CONFIG_COMPILER_OPTIMIZATION_SIZE to get a code-size benefit.

Related: IDF-71, IDF-8286

Closes https://github.com/espressif/esp-idf/issues/18741

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:59:46 +02:00

51 lines
2.4 KiB
CMake

# 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(<output_var> <component_name>...)
#
# 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()