diff --git a/Kconfig b/Kconfig index 308a3e5922a..c8d1d6fc674 100644 --- a/Kconfig +++ b/Kconfig @@ -459,6 +459,21 @@ mainmenu "Espressif IoT Development Framework Configuration" the ZCMP extension for source files that contain functions which may execute while mstatus.mie = 0. + menu "LLVM optimizations" + depends on IDF_TOOLCHAIN_CLANG && IDF_TARGET_ESP32P4 + + config COMPILER_LLVM_MEMCPY_OPTIMIZATION + bool "Optimize memcpy with PIE" + default n + help + Optimize memcpy using the RISC-V PIE extension on ESP32-P4. + Applied only to sources or components opted in with + ENABLE_LLVM_OPT or idf_component_enable_llvm_opt, or by + applying IDF_LLVM_OPT_MEMCPY or IDF_LLVM_OPT_ALL with + target_compile_options. + + endmenu + choice COMPILER_OPTIMIZATION_ASSERTION_LEVEL prompt "Assertion level" default COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE diff --git a/docs/en/api-guides/build-system.rst b/docs/en/api-guides/build-system.rst index f89000e7d52..4cbb125c890 100644 --- a/docs/en/api-guides/build-system.rst +++ b/docs/en/api-guides/build-system.rst @@ -471,6 +471,74 @@ This can be useful if there is upstream code that emits warnings. When using these commands, place them after the call to ``idf_component_register`` in the component CMakeLists file. +.. only:: esp32p4 + + .. _cmake-llvm-optimizations: + + LLVM Optimizations + ^^^^^^^^^^^^^^^^^^ + + ESP-IDF can apply extra LLVM/Clang optimizations to selected sources. Enable the options in menuconfig. Mark a component or a file list in CMake. ESP-IDF sets the corresponding compiler flags, so projects do not write those flags by hand. + + In :ref:`project-configuration-menu`, enable :menuitem:`CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION` under Compiler options > LLVM optimizations. Flags apply only to components or source files you select in CMake. + + Apply to the whole component: + + .. code-block:: cmake + + idf_component_register(SRCS "foo.c" "bar.c" + INCLUDE_DIRS "." + ENABLE_LLVM_OPT) + + Apply to one or more source files (call after ``idf_component_register``): + + .. code-block:: cmake + + idf_component_enable_llvm_opt(SRCS "foo.c" "bar.c") + + Source paths must match the paths passed to ``idf_component_register``. As with ``set_source_files_properties``, source-level selection is not supported with sources discovered through ``SRC_DIRS``. + + Third-party components such as LVGL do not call these helpers. After ``idf_component_register``, get the dependency's library target and apply the exported flags with standard CMake. ``IDF_LLVM_OPT_ALL`` holds the flags for every LLVM optimization enabled in menuconfig; ``IDF_LLVM_OPT_MEMCPY`` holds only the flags for :menuitem:`CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION`. Both are empty when those options are off or the compiler is not Clang: + + Library target of the managed ``lvgl`` component (registry name :code:`lvgl__lvgl`), not its source list: + + .. code-block:: cmake + + idf_component_get_property(lvgl_lib "lvgl__lvgl" COMPONENT_LIB) + if(IDF_LLVM_OPT_ALL) + target_compile_options(${lvgl_lib} PRIVATE ${IDF_LLVM_OPT_ALL}) + endif() + + To apply only the memcpy optimization: + + .. code-block:: cmake + + idf_component_get_property(lvgl_lib "lvgl__lvgl" COMPONENT_LIB) + if(IDF_LLVM_OPT_MEMCPY) + target_compile_options(${lvgl_lib} PRIVATE ${IDF_LLVM_OPT_MEMCPY}) + endif() + + This does not patch the third-party CMakeLists. Do not apply it to every source in a large component; see the limitations below. + + Advanced users can append extra Clang or LLVM flags. These are applied together with any LLVM optimizations enabled in menuconfig. Omit ``SRCS`` to apply them to the whole component: + + .. code-block:: cmake + + idf_component_enable_llvm_opt( + SRCS "foo.c" + OPTIONS "-mllvm=-my-custom-llvm-option") + + The current menuconfig option speeds up ``memcpy`` using the RISC-V PIE extension. Install and select the Espressif Clang toolchain with ``IDF_TOOLCHAIN=clang``; IDF does not substitute flags when the active compiler is GCC. Further menuconfig options can be added later without changing the CMake enable API. See :example:`system/llvm_opt` and :example:`system/llvm_memcpy_opt`. + + Limitations: + + - Requires the Espressif Clang toolchain. ``ENABLE_LLVM_OPT``, ``idf_component_enable_llvm_opt``, ``IDF_LLVM_OPT_ALL``, and ``IDF_LLVM_OPT_MEMCPY`` have no effect when the selected compiler is not Clang. + - Currently validated on ESP32-P4 only. + - Apply only to hot paths. Prefer a measured file list. Results depend on alignment, size, chip revision, and flash/cache layout. + - Do **not** enable this for the whole project or for every source in a large component. The generated code uses the PIE coprocessor. The first PIE instruction in a task that does not currently own PIE traps into the kernel, which lazy-saves the previous owner's registers and restores this task's; that switch repeats for every such task and can make the application slower overall. Extra code size (I-cache) can also hurt, but is secondary. See :doc:`../api-reference/system/freertos_idf`. + - Do not use this memcpy option from an ISR: PIE coprocessor use in interrupt context is not allowed and aborts. + - Do not treat the CMake markers as a global ``-O`` replacement. + .. _component-configuration: @@ -1565,6 +1633,10 @@ The arguments for ``idf_component_register`` include: - KCONFIG_PROJBUILD - override the default Kconfig.projbuild file - WHOLE_ARCHIVE - if specified, the component library is surrounded by ``-Wl,--whole-archive``, ``-Wl,--no-whole-archive`` when linked. This has the same effect as setting ``WHOLE_ARCHIVE`` component property. +.. only:: esp32p4 + + On ESP32-P4, ``idf_component_register`` also accepts ``ENABLE_LLVM_OPT`` to apply LLVM optimizations enabled in menuconfig to all source files in the component. Ignored when the selected compiler is not Clang. See :ref:`cmake-llvm-optimizations`. + The following are used for :ref:`embedding data into the component `, and is considered as source files when determining if a component is config-only. This means that even if the component does not specify source files, a static library is still created internally for the component if it specifies either: - EMBED_FILES - binary files to be embedded in the component diff --git a/docs/zh_CN/api-guides/build-system.rst b/docs/zh_CN/api-guides/build-system.rst index e490c48c78b..6e41284cbd7 100644 --- a/docs/zh_CN/api-guides/build-system.rst +++ b/docs/zh_CN/api-guides/build-system.rst @@ -471,6 +471,74 @@ ESP-IDF 在搜索所有待构建的组件时,会按照以下优先级搜索组 请注意,上述两条命令只能在组件 CMakeLists 文件的 ``idf_component_register`` 命令之后调用。 +.. only:: esp32p4 + + .. _cmake-llvm-optimizations: + + LLVM 优化 + ^^^^^^^^^^^^^^^^^^ + + ESP-IDF 可以把额外的 LLVM/Clang 优化应用到选定的源文件上:在 menuconfig 中启用相应选项,再在 CMake 里标记组件或文件列表。menuconfig 选项对应的编译器标志由 ESP-IDF 设置,项目无需手写这些标志。 + + 在 :ref:`project-configuration-menu` 的 Compiler options > LLVM optimizations 下启用 :menuitem:`CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION`。相关标志仅应用于你在 CMake 中选定的组件或源文件。 + + 应用于整个组件: + + .. code-block:: cmake + + idf_component_register(SRCS "foo.c" "bar.c" + INCLUDE_DIRS "." + ENABLE_LLVM_OPT) + + 应用于一个或多个源文件(在 ``idf_component_register`` 之后调用): + + .. code-block:: cmake + + idf_component_enable_llvm_opt(SRCS "foo.c" "bar.c") + + 源文件路径必须与传递给 ``idf_component_register`` 的路径一致。与 ``set_source_files_properties`` 一样,通过 ``SRC_DIRS`` 查找到的源文件不支持按文件选择优化。 + + LVGL 等第三方组件不会调用上述 helper。在 ``idf_component_register`` 之后,用标准 CMake 命令取出依赖的库 target 并套用导出的标志。``IDF_LLVM_OPT_ALL`` 包含 menuconfig 中已启用的全部 LLVM 优化对应的标志;``IDF_LLVM_OPT_MEMCPY`` 只包含 :menuitem:`CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION` 对应的标志。当对应选项关闭或编译器不是 Clang 时,二者均为空: + + managed ``lvgl`` 组件的库 target(组件仓库名为 :code:`lvgl__lvgl`,不是它的源文件列表): + + .. code-block:: cmake + + idf_component_get_property(lvgl_lib "lvgl__lvgl" COMPONENT_LIB) + if(IDF_LLVM_OPT_ALL) + target_compile_options(${lvgl_lib} PRIVATE ${IDF_LLVM_OPT_ALL}) + endif() + + 若只应用 memcpy 优化: + + .. code-block:: cmake + + idf_component_get_property(lvgl_lib "lvgl__lvgl" COMPONENT_LIB) + if(IDF_LLVM_OPT_MEMCPY) + target_compile_options(${lvgl_lib} PRIVATE ${IDF_LLVM_OPT_MEMCPY}) + endif() + + 这样无需修改第三方的 CMakeLists。不要对大型组件中的全部源文件启用;见下方限制。 + + 高级用户可以追加额外的 Clang 或 LLVM 标志。这些标志会与 menuconfig 中启用的 LLVM 优化一起应用。省略 ``SRCS`` 时,自定义标志将应用于整个组件: + + .. code-block:: cmake + + idf_component_enable_llvm_opt( + SRCS "foo.c" + OPTIONS "-mllvm=-my-custom-llvm-option") + + 当前 menuconfig 选项使用 RISC-V PIE 扩展加速 ``memcpy``。请安装并选用乐鑫 Clang 工具链(``IDF_TOOLCHAIN=clang``);当活动编译器为 GCC 时,IDF 不会注入这些标志。后续可在不改动 CMake 启用 API 的情况下,通过 menuconfig 添加新选项。示例见 :example:`system/llvm_opt` 与 :example:`system/llvm_memcpy_opt`。 + + 使用限制: + + - 需要乐鑫 Clang 工具链。如果所选编译器不是 Clang,``ENABLE_LLVM_OPT``、``idf_component_enable_llvm_opt``、``IDF_LLVM_OPT_ALL`` 和 ``IDF_LLVM_OPT_MEMCPY`` 不会产生任何效果。 + - 当前仅在 ESP32-P4 上完成验证。 + - 仅在优化热点路径时启用。优先对已测量的文件列表启用。结果依赖对齐、长度、芯片修订以及 Flash/Cache 布局。 + - **不要** 对整个工程或大型组件中的全部源文件启用。生成代码会使用 PIE 协处理器:某个尚未拥有 PIE 的任务第一次执行 PIE 指令时会陷入内核,内核惰性保存上一任拥有者的寄存器并恢复当前任务的寄存器;每个这样的任务都会重复该切换,整体应用可能反而变慢。额外代码体积(I-cache)也可能有影响,但是次要因素。详见 :doc:`../api-reference/system/freertos_idf`。 + - 不要在 ISR 中使用该 memcpy 选项:中断上下文中使用 PIE 协处理器不被允许,并会导致运行中止。 + - 不要把 CMake 标记当作全局 ``-O`` 替代。 + .. _component-configuration: @@ -1565,6 +1633,10 @@ ESP-IDF 组件命令 - KCONFIG_PROJBUILD - 覆盖默认的 Kconfig.projbuild 文件。 - WHOLE_ARCHIVE - 如果指定了此参数,链接时会在组件库的前后分别添加 ``-Wl,--whole-archive`` 和 ``-Wl,--no-whole-archive``。这与设置 ``WHOLE_ARCHIVE`` 组件属性的效果一致。 +.. only:: esp32p4 + + 在 ESP32-P4 上,``idf_component_register`` 还接受 ``ENABLE_LLVM_OPT``,用于将 menuconfig 中启用的 LLVM 优化应用于组件中的所有源文件。如果所选编译器不是 Clang,该参数会被忽略。详见 :ref:`cmake-llvm-optimizations`。 + 以下内容用于 :ref:`将数据嵌入到组件中`,并在确定组件是否仅用于配置时被视为源文件。这意味着,即使组件没有指定源文件,如果组件指定了以下其中之一,仍然会在内部为组件创建一个静态库。 - EMBED_FILES - 嵌入组件的二进制文件 diff --git a/examples/system/llvm_memcpy_opt/.build-test-rules.yml b/examples/system/llvm_memcpy_opt/.build-test-rules.yml new file mode 100644 index 00000000000..a087b27f1df --- /dev/null +++ b/examples/system/llvm_memcpy_opt/.build-test-rules.yml @@ -0,0 +1,3 @@ +examples/system/llvm_memcpy_opt: + enable: + - if: IDF_TARGET == "esp32p4" and CONFIG_NAME == "default" diff --git a/examples/system/llvm_memcpy_opt/CMakeLists.txt b/examples/system/llvm_memcpy_opt/CMakeLists.txt new file mode 100644 index 00000000000..f31f385bd8c --- /dev/null +++ b/examples/system/llvm_memcpy_opt/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(llvm_memcpy_opt) diff --git a/examples/system/llvm_memcpy_opt/README.md b/examples/system/llvm_memcpy_opt/README.md new file mode 100644 index 00000000000..a45b9803595 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/README.md @@ -0,0 +1,61 @@ +| Supported Targets | ESP32-P4 | +| ----------------- | -------- | + +# LLVM memcpy optimization + +Benchmark for the ESP-IDF LLVM optimization framework: `menuconfig` enables +the memcpy optimization, and CMake marks which sources receive it. + +This example compares two source files in the same component implementing the +same 16-byte aligned, fixed 1024-byte `memcpy`: + +- `memcpy_baseline.c`: built without LLVM optimization. +- `memcpy_optimized.c`: selected with + `idf_component_enable_llvm_opt(SRCS "memcpy_optimized.c")`. + +The project uses performance optimization. When **Compiler options > LLVM +optimizations > Optimize memcpy with PIE** is enabled, ESP-IDF injects the +framework flags only into `memcpy_optimized.c`. + +Use the Espressif Clang toolchain (`IDF_TOOLCHAIN=clang`) for the memcpy +speedup. With the default GCC toolchain the example still **builds and runs** +(so CI can board-test correctness on ESP32-P4): the LLVM opt-in markers are +ignored and CMake prints a warning with the Clang commands below. Cycle counts +under GCC are not a meaningful “optimized vs baseline” comparison. + +Both ESP32-P4 ECO4 (PIE 2.1) and ECO5+ (PIE 2.2) are supported. The framework +picks the PIE feature from the chip revision selected in menuconfig (IDF +default is ECO5+ / rev >= 3.0). For an ECO4 board, set the revision under +**Component config > ESP System Settings** (or equivalent) to a rev < 3.0 +option so `CONFIG_ESP32P4_SELECTS_REV_LESS_V3` is enabled. No example-specific +revision pin is required. + +## Build and run + +```bash +cd "$IDF_PATH/examples/system/llvm_memcpy_opt" +IDF_TOOLCHAIN=clang idf.py set-target esp32p4 +IDF_TOOLCHAIN=clang idf.py build flash monitor +``` + +`sdkconfig.defaults` enables the menuconfig option for this example. It can +also be changed with: + +```bash +IDF_TOOLCHAIN=clang idf.py menuconfig +``` + +The example verifies both copies against the source, then prints minimum cycle +counts from 512 runs (benchmark helper placed in IRAM). Example ECO4 / PIE 2.1 +output: + +```text +LLVM memcpy optimization: enabled +Copy: 1024 bytes, source/destination alignment: 16 bytes +Baseline cycles: 671 +Optimized cycles: 136 +Result: PASS +``` + +Cycle counts depend on chip revision, clock configuration, toolchain version, +and memory placement. Use values printed by the target as benchmark results. diff --git a/examples/system/llvm_memcpy_opt/main/CMakeLists.txt b/examples/system/llvm_memcpy_opt/main/CMakeLists.txt new file mode 100644 index 00000000000..da509ad6cf9 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/main/CMakeLists.txt @@ -0,0 +1,18 @@ +idf_component_register(SRCS "llvm_memcpy_main.c" + "memcpy_baseline.c" + "memcpy_optimized.c" + INCLUDE_DIRS "") + +# Framework markers are ignored on non-Clang (by design). Keep the example +# buildable under the default GCC example CI jobs so P4 board tests can run; +# print a clear warning so local users still know to use Clang for speedup. +if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang") + message(WARNING + "examples/system/llvm_memcpy_opt: compiler is not Clang; " + "ENABLE_LLVM_OPT / idf_component_enable_llvm_opt are ignored. " + "For the memcpy optimization use:\n" + " IDF_TOOLCHAIN=clang idf.py set-target esp32p4\n" + " IDF_TOOLCHAIN=clang idf.py build flash monitor") +endif() + +idf_component_enable_llvm_opt(SRCS "memcpy_optimized.c") diff --git a/examples/system/llvm_memcpy_opt/main/llvm_memcpy_main.c b/examples/system/llvm_memcpy_opt/main/llvm_memcpy_main.c new file mode 100644 index 00000000000..c1eb3bdf922 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/main/llvm_memcpy_main.c @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include +#include +#include +#include +#include + +#include "esp_attr.h" +#include "esp_cpu.h" +#include "freertos/FreeRTOS.h" +#include "sdkconfig.h" + +#define COPY_SIZE 1024 +#define BENCHMARK_RUNS 512 + +#if CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION +#define LLVM_MEMCPY_STATUS "enabled" +#else +#define LLVM_MEMCPY_STATUS "disabled" +#endif + +void memcpy_baseline_1024(void *dst, const void *src); +void memcpy_optimized_1024(void *dst, const void *src); + +typedef void (*copy_fn_t)(void *, const void *); + +static _Alignas(16) uint8_t s_source[COPY_SIZE]; +static _Alignas(16) uint8_t s_baseline_destination[COPY_SIZE]; +static _Alignas(16) uint8_t s_optimized_destination[COPY_SIZE]; +static portMUX_TYPE s_benchmark_lock = portMUX_INITIALIZER_UNLOCKED; + +static IRAM_ATTR uint32_t benchmark(copy_fn_t copy, uint8_t *destination) +{ + uint32_t best = UINT32_MAX; + + for (int i = 0; i < 16; ++i) { + copy(destination, s_source); + } + + for (int i = 0; i < BENCHMARK_RUNS; ++i) { + portENTER_CRITICAL(&s_benchmark_lock); + __asm__ volatile ("" ::: "memory"); + uint32_t start = esp_cpu_get_cycle_count(); + copy(destination, s_source); + uint32_t end = esp_cpu_get_cycle_count(); + __asm__ volatile ("" ::: "memory"); + portEXIT_CRITICAL(&s_benchmark_lock); + + uint32_t cycles = end - start; + if (cycles < best) { + best = cycles; + } + } + + return best; +} + +void app_main(void) +{ + for (int i = 0; i < COPY_SIZE; ++i) { + s_source[i] = (uint8_t)(i * 3 + 1); + } + + memcpy_baseline_1024(s_baseline_destination, s_source); + memcpy_optimized_1024(s_optimized_destination, s_source); + bool result_matches = memcmp(s_baseline_destination, s_optimized_destination, COPY_SIZE) == 0 + && memcmp(s_source, s_optimized_destination, COPY_SIZE) == 0; + + uint32_t baseline_cycles = benchmark(memcpy_baseline_1024, s_baseline_destination); + uint32_t optimized_cycles = benchmark(memcpy_optimized_1024, s_optimized_destination); + + printf("LLVM memcpy optimization: %s\n", LLVM_MEMCPY_STATUS); + printf("Copy: %d bytes, source/destination alignment: 16 bytes\n", COPY_SIZE); + printf("Baseline cycles: %" PRIu32 "\n", baseline_cycles); + printf("Optimized cycles: %" PRIu32 "\n", optimized_cycles); + printf("Result: %s\n", result_matches ? "PASS" : "FAIL"); +} diff --git a/examples/system/llvm_memcpy_opt/main/memcpy_baseline.c b/examples/system/llvm_memcpy_opt/main/memcpy_baseline.c new file mode 100644 index 00000000000..b892bf3f3b8 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/main/memcpy_baseline.c @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include + +__attribute__((noinline)) +void memcpy_baseline_1024(void *dst, const void *src) +{ + dst = __builtin_assume_aligned(dst, 16); + src = __builtin_assume_aligned(src, 16); + memcpy(dst, src, 1024); +} diff --git a/examples/system/llvm_memcpy_opt/main/memcpy_optimized.c b/examples/system/llvm_memcpy_opt/main/memcpy_optimized.c new file mode 100644 index 00000000000..5792aba2417 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/main/memcpy_optimized.c @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include + +__attribute__((noinline)) +void memcpy_optimized_1024(void *dst, const void *src) +{ + dst = __builtin_assume_aligned(dst, 16); + src = __builtin_assume_aligned(src, 16); + memcpy(dst, src, 1024); +} diff --git a/examples/system/llvm_memcpy_opt/pytest_llvm_memcpy_opt.py b/examples/system/llvm_memcpy_opt/pytest_llvm_memcpy_opt.py new file mode 100644 index 00000000000..cf36ad68575 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/pytest_llvm_memcpy_opt.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@idf_parametrize('target', ['esp32p4'], indirect=['target']) +def test_llvm_memcpy_opt(dut: Dut) -> None: + """Board smoke: correctness PASS. Speedup needs Clang + menuconfig option.""" + dut.expect(r'LLVM memcpy optimization: (enabled|disabled)', timeout=60) + dut.expect(r'Baseline cycles:\s+\d+', timeout=30) + dut.expect(r'Optimized cycles:\s+\d+', timeout=30) + dut.expect_exact('Result: PASS', timeout=30) diff --git a/examples/system/llvm_memcpy_opt/sdkconfig.defaults b/examples/system/llvm_memcpy_opt/sdkconfig.defaults new file mode 100644 index 00000000000..ce974b48ed1 --- /dev/null +++ b/examples/system/llvm_memcpy_opt/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION=y diff --git a/examples/system/llvm_opt/.build-test-rules.yml b/examples/system/llvm_opt/.build-test-rules.yml new file mode 100644 index 00000000000..06596ddc818 --- /dev/null +++ b/examples/system/llvm_opt/.build-test-rules.yml @@ -0,0 +1,3 @@ +examples/system/llvm_opt: + enable: + - if: IDF_TARGET == "esp32p4" and CONFIG_NAME == "default" diff --git a/examples/system/llvm_opt/CMakeLists.txt b/examples/system/llvm_opt/CMakeLists.txt new file mode 100644 index 00000000000..53d1959476c --- /dev/null +++ b/examples/system/llvm_opt/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(llvm_opt) diff --git a/examples/system/llvm_opt/README.md b/examples/system/llvm_opt/README.md new file mode 100644 index 00000000000..0a33aadecda --- /dev/null +++ b/examples/system/llvm_opt/README.md @@ -0,0 +1,43 @@ +| Supported Targets | ESP32-P4 | +| ----------------- | -------- | + +# LLVM optimization (component scope) + +Minimal example for the ESP-IDF LLVM optimization framework. It shows how to +apply the LLVM optimizations selected in menuconfig to **an entire component** +with the `ENABLE_LLVM_OPT` argument of `idf_component_register`: + +```cmake +idf_component_register(SRCS "llvm_opt_main.c" + INCLUDE_DIRS "" + ENABLE_LLVM_OPT) +``` + +Enable the option in menuconfig: **Compiler options > LLVM optimizations > +Optimize memcpy with PIE**. On ESP32-P4 with the Clang toolchain, ESP-IDF then +injects the corresponding compiler flags into every source file of this +component. + +With the default GCC toolchain the example still builds (for CI board smoke +tests); `ENABLE_LLVM_OPT` is ignored and CMake prints a warning. Use Clang for +real LLVM opt flags: + + +## Build and run + +Activate the Clang toolchain before configuring the project: + +```bash +cd "$IDF_PATH/examples/system/llvm_opt" +IDF_TOOLCHAIN=clang idf.py set-target esp32p4 +IDF_TOOLCHAIN=clang idf.py build flash monitor +``` + +`CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION` is enabled by `sdkconfig.defaults` +and can be changed with `idf.py menuconfig`. + +## Related example + +For the file-level API (`idf_component_enable_llvm_opt`) and a `memcpy` +benchmark comparing optimized and baseline sources, see the +`llvm_memcpy_opt` example. diff --git a/examples/system/llvm_opt/main/CMakeLists.txt b/examples/system/llvm_opt/main/CMakeLists.txt new file mode 100644 index 00000000000..4005ef33788 --- /dev/null +++ b/examples/system/llvm_opt/main/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_component_register(SRCS "llvm_opt_main.c" + INCLUDE_DIRS "" + ENABLE_LLVM_OPT) + +if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang") + message(WARNING + "examples/system/llvm_opt: compiler is not Clang; " + "ENABLE_LLVM_OPT is ignored. For real LLVM opt flags use:\n" + " IDF_TOOLCHAIN=clang idf.py set-target esp32p4\n" + " IDF_TOOLCHAIN=clang idf.py build flash monitor") +endif() diff --git a/examples/system/llvm_opt/main/llvm_opt_main.c b/examples/system/llvm_opt/main/llvm_opt_main.c new file mode 100644 index 00000000000..93e406382a7 --- /dev/null +++ b/examples/system/llvm_opt/main/llvm_opt_main.c @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include + +#include "sdkconfig.h" + +void app_main(void) +{ +#if CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION + printf("LLVM optimization for ESP32-P4 memcpy: enabled in menuconfig\n"); +#else + printf("LLVM optimization for ESP32-P4 memcpy: not enabled (select it in menuconfig)\n"); +#endif + printf("This component is built with ENABLE_LLVM_OPT.\n"); +} diff --git a/examples/system/llvm_opt/pytest_llvm_opt.py b/examples/system/llvm_opt/pytest_llvm_opt.py new file mode 100644 index 00000000000..e7fb8c68903 --- /dev/null +++ b/examples/system/llvm_opt/pytest_llvm_opt.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@idf_parametrize('target', ['esp32p4'], indirect=['target']) +def test_llvm_opt(dut: Dut) -> None: + dut.expect(r'LLVM optimization for ESP32-P4 memcpy:', timeout=30) + dut.expect_exact('This component is built with ENABLE_LLVM_OPT.', timeout=10) diff --git a/examples/system/llvm_opt/sdkconfig.defaults b/examples/system/llvm_opt/sdkconfig.defaults new file mode 100644 index 00000000000..ce974b48ed1 --- /dev/null +++ b/examples/system/llvm_opt/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION=y diff --git a/tools/cmake/component.cmake b/tools/cmake/component.cmake index c363770ed92..3f6a8f4e5bd 100644 --- a/tools/cmake/component.cmake +++ b/tools/cmake/component.cmake @@ -1,3 +1,5 @@ +include(${CMAKE_CURRENT_LIST_DIR}/llvm_optimizations.cmake) + # # Internal function for retrieving component properties from a component target. # @@ -440,8 +442,9 @@ endfunction() # @param[in, optional] KCONFIG (single value) override the default Kconfig # @param[in, optional] KCONFIG_PROJBUILD (single value) override the default Kconfig # @param[in, optional] WHOLE_ARCHIVE (option) link the component as --whole-archive +# @param[in, optional] ENABLE_LLVM_OPT (option) enable the LLVM optimizations selected in menuconfig function(idf_component_register) - set(options WHOLE_ARCHIVE) + set(options WHOLE_ARCHIVE ENABLE_LLVM_OPT) set(single_value KCONFIG KCONFIG_PROJBUILD) set(multi_value SRCS SRC_DIRS EXCLUDE_SRCS INCLUDE_DIRS PRIV_INCLUDE_DIRS LDFRAGMENTS REQUIRES @@ -487,9 +490,16 @@ function(idf_component_register) idf_build_get_property(config_dir CONFIG_DIR) + # Publish IDF_LLVM_OPT_* even when this component does not opt in, so the + # application can apply them to a third-party target with standard CMake. + __idf_llvm_opt_publish_flags() + # The contents of 'sources' is from the __component_add_sources call if(sources OR __EMBED_FILES OR __EMBED_TXTFILES) add_library(${component_lib} STATIC ${sources}) + if(__ENABLE_LLVM_OPT) + __idf_apply_llvm_opt_to_target(${component_lib}) + endif() __component_set_property(${component_target} COMPONENT_TYPE LIBRARY) __component_add_include_dirs(${component_lib} "${__INCLUDE_DIRS}" PUBLIC) __component_add_include_dirs(${component_lib} "${__PRIV_INCLUDE_DIRS}" PRIVATE) diff --git a/tools/cmake/llvm_optimizations.cmake b/tools/cmake/llvm_optimizations.cmake new file mode 100644 index 00000000000..857e7932670 --- /dev/null +++ b/tools/cmake/llvm_optimizations.cmake @@ -0,0 +1,140 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: Apache-2.0 + +include_guard(GLOBAL) + +# ESP-IDF LLVM optimization framework: map menuconfig-selected optimizations to +# compiler flags. Component CMakeLists only mark opt-in scopes (file / list / +# component). Flags for each menuconfig option are also published as +# IDF_LLVM_OPT_* for standard CMake (third-party). + +function(__idf_llvm_opt_collect_flags memcpy_out all_out) + set(memcpy "") + set(all "") + + if(CONFIG_IDF_TOOLCHAIN_CLANG AND CMAKE_C_COMPILER_ID MATCHES "Clang") + if(CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION) + list(APPEND memcpy "-mllvm=-riscv-esp32-p4-mem-intrin") + # PIE 2.1 vs 2.2 comes from -mcpu (components/soc/project_include.cmake). + # +espv-lowering is still required for .m intrinsic isel (vld/vst.128.ip.m). + # Do not pass +xespv2p1: that name is not a Clang target-feature and is ignored. + list(APPEND memcpy "-Xclang" "-target-feature" "-Xclang" "+espv-lowering") + endif() + list(APPEND all ${memcpy}) + endif() + + set(${memcpy_out} "${memcpy}" PARENT_SCOPE) + set(${all_out} "${all}" PARENT_SCOPE) +endfunction() + +# SHELL-wrapped lists for target_compile_options. Empty when Kconfig is off or not Clang. +function(__idf_llvm_opt_publish_flags) + __idf_llvm_opt_collect_flags(memcpy_flags all_flags) + __idf_llvm_opt_shell_wrap_xclang("${memcpy_flags}" memcpy_wrapped) + __idf_llvm_opt_shell_wrap_xclang("${all_flags}" all_wrapped) + set(IDF_LLVM_OPT_MEMCPY "${memcpy_wrapped}" CACHE INTERNAL + "Flags for CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION; empty if off or not Clang" FORCE) + set(IDF_LLVM_OPT_ALL "${all_wrapped}" CACHE INTERNAL + "Flags for all LLVM optimizations enabled in menuconfig; empty if none or not Clang" FORCE) +endfunction() + +function(__idf_llvm_opt_get_options output) + set(options ${ARGN}) + __idf_llvm_opt_publish_flags() + + if(NOT CONFIG_IDF_TOOLCHAIN_CLANG OR NOT CMAKE_C_COMPILER_ID MATCHES "Clang") + set(${output} "" PARENT_SCOPE) + return() + endif() + + __idf_llvm_opt_collect_flags(_memcpy all_flags) + list(APPEND options ${all_flags}) + set(${output} "${options}" PARENT_SCOPE) +endfunction() + +# target_compile_options de-duplicates identical tokens, which splits +# "-Xclang" "" pairs. SHELL: keeps each consecutive -Xclang group intact. +# Source COMPILE_OPTIONS are not de-duplicated and do not honour SHELL:. +function(__idf_llvm_opt_shell_wrap_xclang input output) + set(result "") + set(i 0) + list(LENGTH input n) + while(i LESS n) + list(GET input ${i} item) + if(item STREQUAL "-Xclang") + set(group "") + while(i LESS n) + list(GET input ${i} tok) + if(NOT tok STREQUAL "-Xclang") + break() + endif() + math(EXPR nxt "${i} + 1") + if(nxt GREATER_EQUAL n) + message(FATAL_ERROR "LLVM opt flags: -Xclang missing argument") + endif() + list(GET input ${nxt} arg) + if(group STREQUAL "") + set(group "-Xclang ${arg}") + else() + string(APPEND group " -Xclang ${arg}") + endif() + math(EXPR i "${nxt} + 1") + endwhile() + list(APPEND result "SHELL:${group}") + else() + list(APPEND result "${item}") + math(EXPR i "${i} + 1") + endif() + endwhile() + set(${output} "${result}" PARENT_SCOPE) +endfunction() + +function(__idf_apply_llvm_opt_to_target target) + __idf_llvm_opt_get_options(options ${ARGN}) + if(options) + __idf_llvm_opt_shell_wrap_xclang("${options}" options) + target_compile_options(${target} PRIVATE ${options}) + endif() +endfunction() + +function(__idf_apply_llvm_opt_to_sources) + set(multi_value SRCS OPTIONS) + cmake_parse_arguments(ARG "" "" "${multi_value}" ${ARGN}) + + if(ARG_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown idf_component_enable_llvm_opt arguments: ${ARG_UNPARSED_ARGUMENTS}") + endif() + + __idf_llvm_opt_get_options(options ${ARG_OPTIONS}) + if(options) + set_property(SOURCE ${ARG_SRCS} APPEND PROPERTY COMPILE_OPTIONS ${options}) + endif() +endfunction() + +# Apply menuconfig-selected LLVM optimizations to one source, a source list, or +# the whole current component. OPTIONS is an advanced escape hatch for custom +# Clang/LLVM optimization options and is ignored with non-Clang toolchains. +function(idf_component_enable_llvm_opt) + set(multi_value SRCS OPTIONS) + cmake_parse_arguments(ARG "" "" "${multi_value}" ${ARGN}) + + if(ARG_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown idf_component_enable_llvm_opt arguments: ${ARG_UNPARSED_ARGUMENTS}") + endif() + + if(ARG_SRCS) + __idf_apply_llvm_opt_to_sources(SRCS ${ARG_SRCS} OPTIONS ${ARG_OPTIONS}) + return() + endif() + + if(COMPONENT_LIB AND TARGET ${COMPONENT_LIB}) + set(component_target ${COMPONENT_LIB}) + elseif(COMPONENT_TARGET AND TARGET ${COMPONENT_TARGET}) + set(component_target ${COMPONENT_TARGET}) + else() + message(FATAL_ERROR "idf_component_enable_llvm_opt must be called after idf_component_register") + endif() + + __idf_apply_llvm_opt_to_target(${component_target} ${ARG_OPTIONS}) +endfunction() diff --git a/tools/cmake/tests/llvm_optimizations/CMakeLists.txt b/tools/cmake/tests/llvm_optimizations/CMakeLists.txt new file mode 100644 index 00000000000..68cc4332d71 --- /dev/null +++ b/tools/cmake/tests/llvm_optimizations/CMakeLists.txt @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.22) +# Never compiles: only inspects COMPILE_OPTIONS. Host CI (Windows minimal +# cmake jobs) has no C compiler on PATH, so skip compiler detection. +set(CMAKE_C_COMPILER "${CMAKE_COMMAND}" CACHE FILEPATH "" FORCE) +set(CMAKE_C_COMPILER_WORKS TRUE CACHE BOOL "" FORCE) +set(CMAKE_C_COMPILER_FORCED TRUE) +project(llvm_optimizations_test C) + +include(${CMAKE_CURRENT_LIST_DIR}/../../llvm_optimizations.cmake) + +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/component.c "void component(void) {}\n") +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/selected.c "void selected(void) {}\n") +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/plain.c "void plain(void) {}\n") +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gcc.c "void gcc_source(void) {}\n") + +set(CMAKE_C_COMPILER_ID Clang) +set(CONFIG_IDF_TOOLCHAIN_CLANG ON) +set(CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION ON) + +add_library(component STATIC ${CMAKE_CURRENT_BINARY_DIR}/component.c) +set(COMPONENT_LIB component) +idf_component_enable_llvm_opt() +get_target_property(component_options component COMPILE_OPTIONS) +list(JOIN component_options " " component_options) +if(NOT component_options MATCHES "riscv-esp32-p4-mem-intrin") + message(FATAL_ERROR "Component scope is missing the selected LLVM optimization") +endif() +if(NOT component_options MATCHES "SHELL:") + message(FATAL_ERROR "Component scope must SHELL-wrap -Xclang pairs") +endif() +if(component_options MATCHES "xespv2p1") + message(FATAL_ERROR "PIE revision must come from -mcpu, not +xespv2p1") +endif() +if(NOT component_options MATCHES "espv-lowering") + message(FATAL_ERROR "Component scope is missing +espv-lowering for .m intrinsic isel") +endif() + +if(NOT IDF_LLVM_OPT_ALL OR NOT IDF_LLVM_OPT_MEMCPY) + message(FATAL_ERROR "Exported IDF_LLVM_OPT_ALL / IDF_LLVM_OPT_MEMCPY must be set when memcpy is enabled") +endif() +if(NOT "${IDF_LLVM_OPT_ALL}" STREQUAL "${IDF_LLVM_OPT_MEMCPY}") + message(FATAL_ERROR "With only the memcpy optimization enabled, IDF_LLVM_OPT_ALL must equal IDF_LLVM_OPT_MEMCPY") +endif() + +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/third_party.c "void third_party(void) {}\n") +add_library(fake_lvgl STATIC ${CMAKE_CURRENT_BINARY_DIR}/third_party.c) +target_compile_options(fake_lvgl PRIVATE ${IDF_LLVM_OPT_ALL}) +get_target_property(third_party_options fake_lvgl COMPILE_OPTIONS) +list(JOIN third_party_options " " third_party_options) +if(NOT third_party_options MATCHES "riscv-esp32-p4-mem-intrin") + message(FATAL_ERROR "Standard CMake apply of IDF_LLVM_OPT_ALL missed the memcpy flags") +endif() +if(NOT third_party_options MATCHES "SHELL:") + message(FATAL_ERROR "IDF_LLVM_OPT_ALL must be SHELL-wrapped for target_compile_options") +endif() + +add_library(selected STATIC + ${CMAKE_CURRENT_BINARY_DIR}/selected.c + ${CMAKE_CURRENT_BINARY_DIR}/plain.c) +set(COMPONENT_LIB selected) +idf_component_enable_llvm_opt( + SRCS ${CMAKE_CURRENT_BINARY_DIR}/selected.c + OPTIONS "-custom-llvm-option") +get_source_file_property(selected_options ${CMAKE_CURRENT_BINARY_DIR}/selected.c COMPILE_OPTIONS) +get_source_file_property(plain_options ${CMAKE_CURRENT_BINARY_DIR}/plain.c COMPILE_OPTIONS) +list(JOIN selected_options " " selected_options) +if(NOT selected_options MATCHES "riscv-esp32-p4-mem-intrin" + OR NOT selected_options MATCHES "custom-llvm-option") + message(FATAL_ERROR "Source scope is missing selected or custom options") +endif() +if(selected_options MATCHES "SHELL:") + message(FATAL_ERROR "Source COMPILE_OPTIONS must not use SHELL:") +endif() +if(plain_options) + message(FATAL_ERROR "LLVM optimization leaked to an unselected source") +endif() + +set(CMAKE_C_COMPILER_ID GNU) +unset(CONFIG_IDF_TOOLCHAIN_CLANG) +add_library(gcc_component STATIC ${CMAKE_CURRENT_BINARY_DIR}/gcc.c) +set(COMPONENT_LIB gcc_component) +idf_component_enable_llvm_opt(OPTIONS "-custom-llvm-option") +get_target_property(gcc_options gcc_component COMPILE_OPTIONS) +if(gcc_options) + message(FATAL_ERROR "LLVM optimization options must be ignored for non-Clang compilers") +endif() +if(IDF_LLVM_OPT_ALL OR IDF_LLVM_OPT_MEMCPY) + message(FATAL_ERROR "Exported LLVM opt flags must be empty for non-Clang compilers") +endif() diff --git a/tools/cmakev2/compat.cmake b/tools/cmakev2/compat.cmake index 513a146bcf0..e4a82a68c45 100644 --- a/tools/cmakev2/compat.cmake +++ b/tools/cmakev2/compat.cmake @@ -3,6 +3,8 @@ include_guard(GLOBAL) +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/llvm_optimizations.cmake) + #[[ check_expected_tool_version() @@ -607,6 +609,7 @@ endfunction() [REQUIRED_IDF_TARGETS ...] [EMBED_FILES ...] [EMBED_TXTFILES ...] + [ENABLE_LLVM_OPT] [WHOLE_ARCHIVE]) *SRCS[in,opt]* @@ -659,12 +662,16 @@ endfunction() Link the component as --whole-archive. + *ENABLE_LLVM_OPT[in,opt]* + + Enable the LLVM optimizations selected in menuconfig for this component. + Register a new component with the build system using the provided options. This function also automatically links all commonly required and managed components to the component's target. #]] function(idf_component_register) - set(options WHOLE_ARCHIVE) + set(options WHOLE_ARCHIVE ENABLE_LLVM_OPT) set(one_value KCONFIG KCONFIG_PROJBUILD) set(multi_value SRCS SRC_DIRS EXCLUDE_SRCS INCLUDE_DIRS PRIV_INCLUDE_DIRS LDFRAGMENTS REQUIRES @@ -737,8 +744,13 @@ function(idf_component_register) endif() endforeach() + __idf_llvm_opt_publish_flags() + if(sources OR ARG_EMBED_FILES OR ARG_EMBED_TXTFILES) add_library("${COMPONENT_TARGET}" STATIC ${sources}) + if(ARG_ENABLE_LLVM_OPT) + __idf_apply_llvm_opt_to_target("${COMPONENT_TARGET}") + endif() foreach(include_dir IN LISTS include_dirs) target_include_directories("${COMPONENT_TARGET}" PUBLIC "${include_dir}") diff --git a/tools/test_build_system/test_cmake.py b/tools/test_build_system/test_cmake.py index 170ead23a82..ba10257d168 100644 --- a/tools/test_build_system/test_cmake.py +++ b/tools/test_build_system/test_cmake.py @@ -440,3 +440,14 @@ def test_cmake_preset_sdkconfig_defaults_integration(test_app_copy: Path) -> Non sdkconfig_content = sdkconfig_path.read_text() assert 'CONFIG_LWIP_IPV6=y' in sdkconfig_content assert 'CONFIG_ESP_TASK_WDT_TIMEOUT_S=15' in sdkconfig_content + + +def test_cmake_llvm_optimizations_framework(idf_copy: Path) -> None: + logging.info('Test CMake configuration of the LLVM optimization framework') + test_project = idf_copy / 'tools' / 'cmake' / 'tests' / 'llvm_optimizations' + # The standalone test project fakes the compiler ID and CONFIG_* variables to + # verify component/source scope flag selection, custom options, non-leakage + # to unselected sources, exported IDF_LLVM_OPT_* for stock CMake, and the + # non-Clang no-op. run_cmake raises on failure. + # Windows runners default to NMake; nmake is absent. Ninja is on PATH. + run_cmake(str(test_project), '-G', 'Ninja')