feat(compiler): add ESP-IDF LLVM optimization enablement framework

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
chenqian
2026-08-28 11:38:21 +08:00
committed by Chen Qian
parent a3a7642b6a
commit 25046e7959
24 changed files with 742 additions and 2 deletions

15
Kconfig
View File

@@ -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

View File

@@ -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 <cmake_embed_data>`, 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

View File

@@ -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:`将数据嵌入到组件中<cmake_embed_data>`,并在确定组件是否仅用于配置时被视为源文件。这意味着,即使组件没有指定源文件,如果组件指定了以下其中之一,仍然会在内部为组件创建一个静态库。
- EMBED_FILES - 嵌入组件的二进制文件

View File

@@ -0,0 +1,3 @@
examples/system/llvm_memcpy_opt:
enable:
- if: IDF_TARGET == "esp32p4" and CONFIG_NAME == "default"

View File

@@ -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)

View File

@@ -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.

View File

@@ -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")

View File

@@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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");
}

View File

@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <string.h>
__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);
}

View File

@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <string.h>
__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);
}

View File

@@ -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)

View File

@@ -0,0 +1,2 @@
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION=y

View File

@@ -0,0 +1,3 @@
examples/system/llvm_opt:
enable:
- if: IDF_TARGET == "esp32p4" and CONFIG_NAME == "default"

View File

@@ -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)

View File

@@ -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.

View File

@@ -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()

View File

@@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#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");
}

View File

@@ -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)

View File

@@ -0,0 +1,2 @@
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION=y

View File

@@ -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)

View File

@@ -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" "<arg>" 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()

View File

@@ -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()

View File

@@ -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 <target>...]
[EMBED_FILES <file>...]
[EMBED_TXTFILES <file>...]
[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}")

View File

@@ -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')