[llvm-branch-commits] [libc] [libc][cmake] Add linked size probes to check-libc-size (report only) (PR #233199)
Prabhu Rajasekaran via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Sat Oct 17 23:45:22 PDT 2026
https://github.com/Prabhuk created https://github.com/llvm/llvm-project/pull/233199
The object level checks show what a single entrypoint contains, but not
what a program pays for calling a function: compiler runtime helpers,
other libc objects, and code shared between functions. Add a report of
linked minimal programs ("probes") to check-libc-size in full builds.
For every function in libc/utils/size_check/probes/math.txt, and for a
few combinations such as logf+expf+powf, libc_size_check.py probes
generates a C program whose entry point calls the functions through
volatile globals, compiles it with the target flags and links it with
-nostdlib -static -Wl,--gc-sections -Wl,-e,<entry> -Wl,-Map=...
probe.o libm.a libc.a <builtins>
without startup code or a linker script. It reports, relative to an
empty baseline program, the flash and RAM size, the part of flash that
comes from the libc archives (from the map file), the remainder (mostly
compiler runtime builtins), the size of the named tables, and the libc
objects that were pulled in.
The probes use the same compiler runtime builtins as the hermetic tests
(LIBC_TEST_BUILTINS_LIBRARY, LIBC_TEST_BUILTINS_TARGET or
-print-libgcc-file-name). If there is none, probes that need it are
reported as skipped instead of being linked with unresolved symbols.
The report is informational for now; no size budgets are enforced. The
target only fails if the empty baseline cannot be linked.
Assisted-by: Gemini
---
<sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
>From 528f3f905828dd6a7a4afe0847e7593307b4f279 Mon Sep 17 00:00:00 2001
From: prabhukr <prabhukr at google.com>
Date: Sat, 17 Oct 2026 17:21:52 +0000
Subject: [PATCH] [libc][cmake] Add linked size probes to check-libc-size
(report only)
The object level checks show what a single entrypoint contains, but not
what a program pays for calling a function: compiler runtime helpers,
other libc objects, and code shared between functions. Add a report of
linked minimal programs ("probes") to check-libc-size in full builds.
For every function in libc/utils/size_check/probes/math.txt, and for a
few combinations such as logf+expf+powf, libc_size_check.py probes
generates a C program whose entry point calls the functions through
volatile globals, compiles it with the target flags and links it with
-nostdlib -static -Wl,--gc-sections -Wl,-e,<entry> -Wl,-Map=...
probe.o libm.a libc.a <builtins>
without startup code or a linker script. It reports, relative to an
empty baseline program, the flash and RAM size, the part of flash that
comes from the libc archives (from the map file), the remainder (mostly
compiler runtime builtins), the size of the named tables, and the libc
objects that were pulled in.
The probes use the same compiler runtime builtins as the hermetic tests
(LIBC_TEST_BUILTINS_LIBRARY, LIBC_TEST_BUILTINS_TARGET or
-print-libgcc-file-name). If there is none, probes that need it are
reported as skipped instead of being linked with unresolved symbols.
The report is informational for now; no size budgets are enforced. The
target only fails if the empty baseline cannot be linked.
Assisted-by: Gemini
---
.../modules/LLVMLibCSizeCheckRules.cmake | 117 ++++-
libc/utils/size_check/CMakeLists.txt | 27 ++
libc/utils/size_check/README.md | 40 +-
libc/utils/size_check/libc_size_check.py | 403 +++++++++++++++++-
libc/utils/size_check/libc_size_check_test.py | 152 +++++++
libc/utils/size_check/probes/math.txt | 33 ++
6 files changed, 754 insertions(+), 18 deletions(-)
create mode 100644 libc/utils/size_check/probes/math.txt
diff --git a/libc/cmake/modules/LLVMLibCSizeCheckRules.cmake b/libc/cmake/modules/LLVMLibCSizeCheckRules.cmake
index d1b5b4ed92780..41a99c0acf5f0 100644
--- a/libc/cmake/modules/LLVMLibCSizeCheckRules.cmake
+++ b/libc/cmake/modules/LLVMLibCSizeCheckRules.cmake
@@ -38,15 +38,8 @@ function(_libc_size_check_dump_macros output_file)
list(APPEND cmd "--target=${CMAKE_CXX_COMPILER_TARGET}")
endif()
separate_arguments(cxx_flags NATIVE_COMMAND "${CMAKE_CXX_FLAGS}")
- list(APPEND cmd ${cxx_flags})
- foreach(opt IN LISTS ARGN)
- if(opt MATCHES "^SHELL:(.*)$")
- separate_arguments(shell_opts NATIVE_COMMAND "${CMAKE_MATCH_1}")
- list(APPEND cmd ${shell_opts})
- else()
- list(APPEND cmd ${opt})
- endif()
- endforeach()
+ _libc_size_check_expand_options(options ${ARGN})
+ list(APPEND cmd ${cxx_flags} ${options})
list(APPEND cmd -dM -E -x c++ /dev/null)
get_filename_component(output_dir ${output_file} DIRECTORY)
file(MAKE_DIRECTORY ${output_dir})
@@ -159,3 +152,109 @@ function(add_libc_object_size_check name)
add_dependencies(${name} ${object_targets})
endif()
endfunction()
+
+# Expands "SHELL:" prefixed options into separate arguments.
+function(_libc_size_check_expand_options output_var)
+ set(result "")
+ foreach(opt IN LISTS ARGN)
+ if(opt MATCHES "^SHELL:(.*)$")
+ separate_arguments(shell_opts NATIVE_COMMAND "${CMAKE_MATCH_1}")
+ list(APPEND result ${shell_opts})
+ else()
+ list(APPEND result ${opt})
+ endif()
+ endforeach()
+ set(${output_var} ${result} PARENT_SCOPE)
+endfunction()
+
+# Adds a report of the size of minimal programs that call the functions in
+# `PROBES` and are linked against the archives in `ARCHIVE_TARGETS`. The
+# report is informational: the target only fails if the probes cannot be
+# built at all. Probes that need the compiler runtime builtins are skipped
+# when no builtins library is available.
+#
+# Usage:
+# add_libc_size_probes(
+# <name>
+# PROBES <probe list file>
+# MANIFEST <object manifest of add_libc_object_size_check>
+# ARCHIVE_TARGETS <libc archive targets>
+# )
+function(add_libc_size_probes name)
+ cmake_parse_arguments(
+ "SIZE_PROBES"
+ "" # Optional arguments
+ "PROBES;MANIFEST" # Single value arguments
+ "ARCHIVE_TARGETS" # Multi value arguments
+ ${ARGN}
+ )
+
+ set(flags "")
+ if(CMAKE_C_COMPILER_TARGET)
+ list(APPEND flags "--target=${CMAKE_C_COMPILER_TARGET}")
+ endif()
+ separate_arguments(c_flags NATIVE_COMMAND "${CMAKE_C_FLAGS}")
+ _libc_size_check_expand_options(default_options ${LIBC_COMPILE_OPTIONS_DEFAULT})
+ list(APPEND flags ${c_flags} ${default_options})
+ _libc_size_check_expand_options(link_flags ${LIBC_LINK_OPTIONS_DEFAULT})
+ if(LLVM_USE_LINKER)
+ list(APPEND link_flags "-fuse-ld=${LLVM_USE_LINKER}")
+ endif()
+
+ # Use the same compiler runtime builtins as the hermetic tests.
+ set(builtins "")
+ set(builtins_target "")
+ if(LIBC_TEST_BUILTINS_LIBRARY)
+ set(builtins ${LIBC_TEST_BUILTINS_LIBRARY})
+ elseif(LIBC_TEST_BUILTINS_TARGET)
+ set(builtins "$<TARGET_FILE:${LIBC_TEST_BUILTINS_TARGET}>")
+ set(builtins_target ${LIBC_TEST_BUILTINS_TARGET})
+ else()
+ execute_process(
+ COMMAND ${CMAKE_C_COMPILER} ${flags} ${link_flags} -print-libgcc-file-name
+ OUTPUT_VARIABLE libgcc_file
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ RESULT_VARIABLE result
+ ERROR_QUIET
+ )
+ if(result EQUAL 0 AND EXISTS "${libgcc_file}")
+ set(builtins ${libgcc_file})
+ else()
+ message(STATUS "libc size probes: no compiler runtime builtins library "
+ "found; probes that need it will be skipped. Set "
+ "LIBC_TEST_BUILTINS_LIBRARY or LIBC_TEST_BUILTINS_TARGET to link them.")
+ endif()
+ endif()
+
+ set(probe_args "")
+ foreach(flag IN LISTS flags)
+ list(APPEND probe_args "--cflag=${flag}")
+ endforeach()
+ foreach(flag IN LISTS link_flags)
+ list(APPEND probe_args "--ldflag=${flag}")
+ endforeach()
+ foreach(archive IN LISTS SIZE_PROBES_ARCHIVE_TARGETS)
+ list(APPEND probe_args "--archive=$<TARGET_FILE:${archive}>")
+ endforeach()
+
+ add_custom_target(
+ ${name}
+ COMMAND
+ ${Python3_EXECUTABLE} ${LIBC_SIZE_CHECK_SCRIPT} probes
+ --probes ${SIZE_PROBES_PROBES}
+ --manifest ${SIZE_PROBES_MANIFEST}
+ --work-dir ${CMAKE_CURRENT_BINARY_DIR}/${name}
+ --cc ${CMAKE_C_COMPILER}
+ ${probe_args}
+ "--builtins=${builtins}"
+ --nm ${LIBC_SIZE_CHECK_NM}
+ --readelf ${LIBC_SIZE_CHECK_READELF}
+ --json ${CMAKE_CURRENT_BINARY_DIR}/${name}.json
+ --markdown ${CMAKE_CURRENT_BINARY_DIR}/${name}.md
+ DEPENDS ${SIZE_PROBES_PROBES} ${SIZE_PROBES_MANIFEST} ${LIBC_SIZE_CHECK_SCRIPT}
+ COMMENT "Linking libc size probes"
+ USES_TERMINAL
+ VERBATIM
+ )
+ add_dependencies(${name} ${SIZE_PROBES_ARCHIVE_TARGETS} ${builtins_target})
+endfunction()
diff --git a/libc/utils/size_check/CMakeLists.txt b/libc/utils/size_check/CMakeLists.txt
index c73f1ac279c3d..c3cf32c7716e0 100644
--- a/libc/utils/size_check/CMakeLists.txt
+++ b/libc/utils/size_check/CMakeLists.txt
@@ -36,3 +36,30 @@ add_dependencies(
check-libc-size-unittests
libc-size-check-math-objects
)
+
+# Linked size probes need the full build archives. The archive targets are
+# defined later (in libc/lib), so mirror the conditions they are created with.
+_libc_size_check_find_tool(LIBC_SIZE_CHECK_READELF llvm-readelf)
+set(probe_archives "")
+if(LLVM_LIBC_FULL_BUILD AND NOT LLVM_LIBC_ENABLE_MINOR_VARIANT)
+ if(TARGET_LIBM_ENTRYPOINTS)
+ list(APPEND probe_archives libm)
+ endif()
+ if(TARGET_LIBC_ENTRYPOINTS)
+ list(APPEND probe_archives libc)
+ endif()
+endif()
+if(NOT probe_archives)
+ message(STATUS "libc size probes need a full build; skipping them.")
+elseif(NOT LIBC_SIZE_CHECK_READELF)
+ message(WARNING "libc size probes need llvm-readelf; set "
+ "LIBC_SIZE_CHECK_READELF to enable them.")
+else()
+ add_libc_size_probes(
+ libc-size-probes-math
+ PROBES ${CMAKE_CURRENT_SOURCE_DIR}/probes/math.txt
+ MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/libc-size-check-math-objects.manifest
+ ARCHIVE_TARGETS ${probe_archives}
+ )
+ add_dependencies(check-libc-size libc-size-probes-math)
+endif()
diff --git a/libc/utils/size_check/README.md b/libc/utils/size_check/README.md
index 37f30f2ae132a..fbfacf51bb87c 100644
--- a/libc/utils/size_check/README.md
+++ b/libc/utils/size_check/README.md
@@ -7,8 +7,8 @@ configuration that asked for `LIBC_MATH_SMALL_TABLES` or
`LIBC_MATH_SKIP_ACCURATE_PASS`.
The checks never execute target code. They only inspect objects with
-`llvm-nm` and `llvm-size`, so they work the same way for host, cross and
-baremetal builds.
+`llvm-nm` and `llvm-size` (and link small programs, see below), so they work
+the same way for host, cross and baremetal builds.
## Running the checks
@@ -45,6 +45,42 @@ Some entrypoints are compiled with extra target options (for example `-mfma`
on x86-64 when the host supports it), so the predefined macros used by
`when_macros` are dumped once per distinct set of compile options.
+## Linked size probes
+
+In full builds, `check-libc-size` also links a minimal program for every
+function in `probes/math.txt` (plus a few combinations such as
+`logf+expf+powf`) against `libm.a` and `libc.a`, and reports how much code and
+data each one pulls in relative to an empty program:
+
+```
+probe flash libc other tables ram libc objects
+logf 5636 2548 3088 2048 0 logf.cpp.obj
+...
+```
+
+* `flash`: allocated sections with contents (code and read-only data).
+* `libc`: the part of `flash` that comes from the libc archives.
+* `other`: the rest, mostly compiler runtime builtins (e.g. soft-float helpers).
+* `tables`: the size of the named data objects, which does not depend on the
+ compiler.
+* `ram`: `.data` and `.bss`.
+
+The probes are linked with `-nostdlib -static -Wl,--gc-sections` and without
+startup code or a linker script, with the probe entry point as the ELF entry,
+so only the code reachable from the probed functions is measured. The report
+is written to `<build>/libc/utils/size_check/libc-size-probes-math.{json,md}`;
+the probe sources, objects, map files and ELF files are kept in
+`libc-size-probes-math/` next to it.
+
+The probes are report-only for now: they never fail the build unless the
+empty baseline program cannot be linked. They use the same compiler runtime
+builtins as the hermetic tests (`LIBC_TEST_BUILTINS_LIBRARY`,
+`LIBC_TEST_BUILTINS_TARGET`, or `-print-libgcc-file-name`). When no builtins
+library is available, the probes that need one are reported as skipped. To
+build the builtins in the same build, add `compiler-rt` to
+`LLVM_ENABLE_RUNTIMES` and set `LIBC_TEST_BUILTINS_TARGET` (see the hexagon
+cache for an example).
+
## Updating the rules
When a change legitimately grows a function, update `rules/math.json` in the
diff --git a/libc/utils/size_check/libc_size_check.py b/libc/utils/size_check/libc_size_check.py
index 9ced35863bae1..3870a8b669b61 100644
--- a/libc/utils/size_check/libc_size_check.py
+++ b/libc/utils/size_check/libc_size_check.py
@@ -7,15 +7,19 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ==-------------------------------------------------------------------------==#
-"""Structural binary size checks for LLVM libc entrypoint objects.
+"""Binary size checks for LLVM libc.
-The checker reads the symbol table (``llvm-nm -S -C``) and the section sizes
-(``llvm-size -A``) of each entrypoint object listed in a manifest and evaluates
-the rules from a JSON rules file against them. Rules are selected by the active
-``LIBC_CONF_MATH_OPTIMIZATIONS`` flags and by the target's predefined macros, so
-the same rules file can be used for every ELF target and configuration.
+The `objects` command reads the symbol table (``llvm-nm -S -C``) and the
+section sizes (``llvm-size -A``) of each entrypoint object listed in a manifest
+and evaluates the rules from a JSON rules file against them. Rules are selected
+by the active ``LIBC_CONF_MATH_OPTIMIZATIONS`` flags and by the target's
+predefined macros, so the same rules file can be used for every ELF target and
+configuration.
-See README.md in this directory for the rules file format.
+The `probes` command links minimal programs that call a few functions against
+the libc archives and reports how much code and data they pull in.
+
+See README.md in this directory for details.
"""
import argparse
@@ -545,6 +549,371 @@ def evaluate(rules_doc: dict, objects: List[ObjectInfo], ctx: RuleContext):
return findings, summary
+# ===----------------------------------------------------------------------===#
+# Linked size probes.
+# ===----------------------------------------------------------------------===#
+
+PROBE_ENTRY = "__libc_size_probe_entry"
+BASELINE_PROBE = "baseline"
+
+# C types the probe generator can pass to and receive from a function.
+PROBE_TYPES = ("float", "double", "long double", "int", "long")
+
+
+class Prototype:
+ def __init__(self, ret: str, name: str, args: List[str]):
+ self.ret = ret
+ self.name = name
+ self.args = args
+
+ def declaration(self) -> str:
+ return "%s %s(%s);" % (self.ret, self.name, ", ".join(self.args) or "void")
+
+
+def _probe_var(c_type: str) -> str:
+ return "__libc_size_probe_" + c_type.replace(" ", "_")
+
+
+def parse_probe_list(
+ text: str,
+) -> Tuple[Dict[str, Prototype], List[Tuple[str, List[str]]]]:
+ """Parses a probe list.
+
+ Every line of the form ``<type> <name>(<types>);`` declares a function and
+ adds a probe that calls it. A line ``<name>+<name>[+...]`` adds a probe that
+ calls several previously declared functions, which shows how much code
+ they share. ``#`` starts a comment.
+
+ Returns the prototypes and the list of (probe name, functions).
+ """
+ protos: Dict[str, Prototype] = {}
+ probes: List[Tuple[str, List[str]]] = []
+ proto_re = re.compile(r"^(.+?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*;$")
+ for lineno, line in enumerate(text.splitlines(), 1):
+ line = line.split("#", 1)[0].strip()
+ if not line:
+ continue
+ m = proto_re.match(line)
+ if m:
+ ret, name, args = m.group(1).strip(), m.group(2), m.group(3).strip()
+ arg_types = [a.strip() for a in args.split(",")] if args else []
+ if arg_types == ["void"]:
+ arg_types = []
+ for t in [ret] + arg_types:
+ if t not in PROBE_TYPES:
+ raise ValueError("line %d: unsupported type '%s'" % (lineno, t))
+ protos[name] = Prototype(ret, name, arg_types)
+ probes.append((name, [name]))
+ continue
+ fns = [f.strip() for f in line.split("+")]
+ if len(fns) < 2 or not all(f in protos for f in fns):
+ raise ValueError("line %d: cannot parse '%s'" % (lineno, line))
+ probes.append(("+".join(fns), fns))
+ return protos, probes
+
+
+def generate_probe_source(functions: List[str], protos: Dict[str, Prototype]) -> str:
+ """Generates a C program whose entry point calls `functions`.
+
+ The arguments and results go through volatile globals so that the calls
+ cannot be optimized away. The same globals and the errno stub are part of
+ every probe, including the baseline, so they cancel out in the deltas.
+ """
+ out = ["/* Generated by libc_size_check.py. */"]
+ for fn in functions:
+ out.append(protos[fn].declaration())
+ # 'retain' keeps the globals and the errno stub alive with --gc-sections
+ # even when they are not referenced, so that they cancel out exactly.
+ keep = "__attribute__((used, retain))"
+ for t in PROBE_TYPES:
+ out.append("%s volatile %s %s;" % (keep, t, _probe_var(t)))
+ # Needed with LIBC_ERRNO_MODE_EXTERNAL. Weak so that a definition in libc
+ # takes precedence.
+ out.append("%s __attribute__((weak)) int *__llvm_libc_errno(void) {" % keep)
+ out.append(" static int e;")
+ out.append(" return &e;")
+ out.append("}")
+ out.append("void %s(void) {" % PROBE_ENTRY)
+ for fn in functions:
+ p = protos[fn]
+ call = "%s(%s)" % (fn, ", ".join(_probe_var(a) for a in p.args))
+ out.append(" %s = %s;" % (_probe_var(p.ret), call))
+ out.append("}")
+ return "\n".join(out) + "\n"
+
+
+class Section:
+ def __init__(self, name: str, sec_type: str, size: int, flags: str):
+ self.name = name
+ self.type = sec_type
+ self.size = size
+ self.flags = flags
+
+ def is_alloc(self) -> bool:
+ return "A" in self.flags
+
+ def is_flash(self) -> bool:
+ return self.is_alloc() and self.type != "NOBITS"
+
+ def is_ram(self) -> bool:
+ return self.is_alloc() and "W" in self.flags
+
+
+def parse_readelf_sections(text: str) -> List[Section]:
+ """Parses the section headers printed by ``llvm-readelf -S -W``."""
+ sections = []
+ for line in text.splitlines():
+ m = re.match(r"^\s*\[\s*\d+\]\s(.*)$", line)
+ if not m:
+ continue
+ fields = m.group(1).split()
+ # Name Type Address Off Size ES [Flg] Lk Inf Al
+ if len(fields) == 10:
+ name, sec_type, size, flags = fields[0], fields[1], fields[4], fields[6]
+ elif len(fields) == 9:
+ name, sec_type, size, flags = fields[0], fields[1], fields[4], ""
+ else:
+ continue # The NULL section has no name.
+ sections.append(Section(name, sec_type, int(size, 16), flags))
+ return sections
+
+
+_MAP_LINE_RE = re.compile(
+ r"^\s*([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+(\d+) ( *)(\S.*)$"
+)
+
+
+def parse_lld_map(text: str) -> List[Tuple[str, str, int]]:
+ """Parses an lld map file into (output section, input section, size).
+
+ Only input section lines are returned; symbol lines are skipped.
+ """
+ entries = []
+ output = None
+ for line in text.splitlines():
+ m = _MAP_LINE_RE.match(line)
+ if not m:
+ continue
+ depth = len(m.group(5)) // 8
+ if depth == 0:
+ output = m.group(6).strip()
+ elif depth == 1 and output is not None:
+ entries.append((output, m.group(6).strip(), int(m.group(3), 16)))
+ return entries
+
+
+def probe_metrics(
+ sections: List[Section],
+ map_entries: List[Tuple[str, str, int]],
+ data_symbols: List[Symbol],
+ archives: List[str],
+) -> dict:
+ """Computes the size metrics of a linked probe.
+
+ flash: allocated sections with file contents (code and read-only data).
+ ram: allocated writable sections (.data and .bss).
+ libc_flash: the part of `flash` that comes from the libc archives.
+ named_data: total size of the named data objects (tables), which does not
+ depend on the compiler.
+
+ flash and ram are sums of input sections, so that alignment padding, which
+ depends on the section order, does not add noise.
+ """
+ flash_sections = {s.name for s in sections if s.is_flash()}
+ ram_sections = {s.name for s in sections if s.is_ram()}
+ flash = ram = libc_flash = 0
+ members = set()
+ for output, inp, size in map_entries:
+ if output in flash_sections:
+ flash += size
+ if output in ram_sections:
+ ram += size
+ for archive in archives:
+ if inp.startswith(archive + "("):
+ member = inp[len(archive) + 1 :].split(")", 1)[0]
+ members.add("%s(%s)" % (os.path.basename(archive), member))
+ if output in flash_sections:
+ libc_flash += size
+ return {
+ "flash": flash,
+ "ram": ram,
+ "libc_flash": libc_flash,
+ "named_data": sum(s.size for s in data_symbols),
+ "members": sorted(members),
+ }
+
+
+def probe_delta(metrics: dict, baseline: dict) -> dict:
+ delta = {}
+ for key in ("flash", "ram", "libc_flash", "named_data"):
+ delta[key] = metrics[key] - baseline[key]
+ delta["other_flash"] = delta["flash"] - delta["libc_flash"]
+ delta["members"] = [m for m in metrics["members"] if m not in baseline["members"]]
+ return delta
+
+
+_UNDEFINED_RE = re.compile(r"undefined symbol: (\S+)")
+
+
+class ProbeBuilder:
+ """Compiles and links probe programs and measures them."""
+
+ def __init__(self, args):
+ self.args = args
+ self.archives = [a for a in args.archive if a]
+
+ def _run(self, cmd: List[str]) -> subprocess.CompletedProcess:
+ return subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ universal_newlines=True,
+ )
+
+ def build(self, name: str, source: str) -> dict:
+ """Builds one probe. Returns its metrics or a 'skipped' reason."""
+ base = os.path.join(self.args.work_dir, name.replace("+", "_"))
+ write_file(base + ".c", source)
+ compile_cmd = [self.args.cc] + self.args.cflag
+ compile_cmd += ["-Os", "-ffunction-sections", "-fdata-sections"]
+ compile_cmd += ["-ffreestanding", "-fno-builtin"]
+ compile_cmd += ["-fno-asynchronous-unwind-tables", "-fno-unwind-tables"]
+ compile_cmd += ["-c", base + ".c", "-o", base + ".o"]
+ result = self._run(compile_cmd)
+ if result.returncode != 0:
+ raise RuntimeError("compiling %s failed:\n%s" % (name, result.stdout))
+ link_cmd = [self.args.cc] + self.args.cflag + self.args.ldflag
+ link_cmd += ["-nostdlib", "-static", "-Wl,--gc-sections", "-Wl,-z,norelro"]
+ link_cmd += ["-Wl,-e," + PROBE_ENTRY, "-Wl,-Map=" + base + ".map"]
+ link_cmd += [base + ".o", "-Wl,--start-group"] + self.archives
+ link_cmd += ["-Wl,--end-group"]
+ if self.args.builtins:
+ link_cmd.append(self.args.builtins)
+ link_cmd += ["-o", base + ".elf"]
+ result = self._run(link_cmd)
+ if result.returncode != 0:
+ undefined = sorted(set(_UNDEFINED_RE.findall(result.stdout)))
+ if undefined and not self.args.builtins:
+ return {
+ "skipped": "unresolved symbols and no compiler runtime builtins "
+ "library: %s"
+ % ", ".join(undefined[:4] + (["..."] if len(undefined) > 4 else []))
+ }
+ raise RuntimeError("linking %s failed:\n%s" % (name, result.stdout))
+ sections = parse_readelf_sections(
+ run_tool([self.args.readelf, "-S", "-W", base + ".elf"])
+ )
+ defined, _ = parse_nm(
+ run_tool([self.args.nm, "-S", "-C", base + ".elf"]),
+ re.compile(DEFAULT_SYMBOL_PREFIX_REGEX),
+ )
+ data = [s for s in defined if s.is_data()]
+ map_entries = parse_lld_map(read_file(base + ".map"))
+ return probe_metrics(sections, map_entries, data, self.archives)
+
+
+def format_probe_text(results: List[Tuple[str, dict]]) -> str:
+ lines = [
+ "%-24s %8s %8s %8s %8s %6s %s"
+ % ("probe", "flash", "libc", "other", "tables", "ram", "libc objects")
+ ]
+ for name, r in results:
+ if "skipped" in r:
+ lines.append("%-24s skipped: %s" % (name, r["skipped"]))
+ continue
+ lines.append(
+ "%-24s %8d %8d %8d %8d %6d %s"
+ % (
+ name,
+ r["flash"],
+ r["libc_flash"],
+ r["other_flash"],
+ r["named_data"],
+ r["ram"],
+ " ".join(m.split("(", 1)[1].rstrip(")") for m in r["members"]),
+ )
+ )
+ return "\n".join(lines)
+
+
+def format_probe_markdown(results: List[Tuple[str, dict]], title: str) -> str:
+ out = ["# %s" % title, ""]
+ out.append(
+ "Sizes in bytes relative to an empty program. `libc` is the part of "
+ "`flash` that comes from the libc archives, `other` is mostly compiler "
+ "runtime builtins, `tables` is the size of the named data objects."
+ )
+ out += ["", "| probe | flash | libc | other | tables | ram | status |"]
+ out.append("|---|---:|---:|---:|---:|---:|---|")
+ for name, r in results:
+ if "skipped" in r:
+ out.append("| %s | | | | | | skipped: %s |" % (name, r["skipped"]))
+ else:
+ out.append(
+ "| %s | %d | %d | %d | %d | %d | ok |"
+ % (
+ name,
+ r["flash"],
+ r["libc_flash"],
+ r["other_flash"],
+ r["named_data"],
+ r["ram"],
+ )
+ )
+ return "\n".join(out) + "\n"
+
+
+def cmd_probes(args) -> int:
+ protos, probes = parse_probe_list(read_file(args.probes))
+ available = None
+ if args.manifest:
+ available = {e.function for e in parse_manifest(read_file(args.manifest))}
+ os.makedirs(args.work_dir, exist_ok=True)
+ builder = ProbeBuilder(args)
+ baseline = builder.build(BASELINE_PROBE, generate_probe_source([], protos))
+ if "skipped" in baseline:
+ raise RuntimeError(
+ "the baseline probe failed to link: %s" % baseline["skipped"]
+ )
+
+ def build(probe):
+ name, fns = probe
+ missing = [f for f in fns if available is not None and f not in available]
+ if missing:
+ return name, {"skipped": "not in the build: %s" % ", ".join(missing)}
+ result = builder.build(name, generate_probe_source(fns, protos))
+ return name, result if "skipped" in result else probe_delta(result, baseline)
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool:
+ results = list(pool.map(build, probes))
+
+ print("libc size probes (report only): sizes in bytes relative to an empty program")
+ if not args.builtins:
+ print(
+ "note: no compiler runtime builtins library; probes that need one are "
+ "skipped (set LIBC_TEST_BUILTINS_LIBRARY or LIBC_TEST_BUILTINS_TARGET)"
+ )
+ print(format_probe_text(results))
+ if args.json:
+ write_file(
+ args.json,
+ json.dumps(
+ {"baseline": baseline, "probes": dict(results)},
+ indent=2,
+ sort_keys=True,
+ )
+ + "\n",
+ )
+ if args.markdown:
+ write_file(args.markdown, format_probe_markdown(results, "libc size probes"))
+ skipped = sum(1 for _, r in results if "skipped" in r)
+ print(
+ "libc size probes: %d probes linked, %d skipped."
+ % (len(results) - skipped, skipped)
+ )
+ return 0
+
+
# ===----------------------------------------------------------------------===#
# Driver.
# ===----------------------------------------------------------------------===#
@@ -792,6 +1161,26 @@ def build_parser() -> argparse.ArgumentParser:
)
p.add_argument("-j", "--jobs", type=int, default=os.cpu_count() or 1)
p.set_defaults(func=cmd_objects)
+
+ p = sub.add_parser("probes", help="link and measure minimal programs")
+ p.add_argument("--probes", required=True, help="probe list, see parse_probe_list()")
+ p.add_argument("--work-dir", required=True, help="directory for the probes")
+ p.add_argument(
+ "--manifest", help="object manifest; probes of other functions are skipped"
+ )
+ p.add_argument("--cc", required=True, help="C compiler driver")
+ p.add_argument("--cflag", action="append", default=[], help="compile and link flag")
+ p.add_argument("--ldflag", action="append", default=[], help="link flag")
+ p.add_argument(
+ "--archive", action="append", default=[], help="libc archive to link"
+ )
+ p.add_argument("--builtins", default="", help="compiler runtime builtins library")
+ p.add_argument("--nm", default="llvm-nm", help="path to llvm-nm")
+ p.add_argument("--readelf", default="llvm-readelf", help="path to llvm-readelf")
+ p.add_argument("--json", help="write a JSON report to this file")
+ p.add_argument("--markdown", help="write a markdown report to this file")
+ p.add_argument("-j", "--jobs", type=int, default=os.cpu_count() or 1)
+ p.set_defaults(func=cmd_probes)
return parser
diff --git a/libc/utils/size_check/libc_size_check_test.py b/libc/utils/size_check/libc_size_check_test.py
index 416e47f65de5a..b06352b0e96dd 100644
--- a/libc/utils/size_check/libc_size_check_test.py
+++ b/libc/utils/size_check/libc_size_check_test.py
@@ -399,6 +399,158 @@ def test_text_and_markdown(self):
self.assertIn("| logf |", md)
+ARM_LIBM = "/b/libc/lib/libm.a"
+ARM_LIBC = "/b/libc/lib/libc.a"
+BUILTINS = "/tc/lib/armv7em-unknown-none-eabi/libclang_rt.builtins.a"
+
+# lld -Map output of a probe calling logf (ARM, 32-bit addresses).
+MAP_LOGF_ARM = """\
+ VMA LMA Size Align Out In Symbol
+ 100f4 100f4 10 4 .ARM.exidx
+ 100f4 100f4 10 4 <internal>:(.ARM.exidx)
+ 10108 10108 400 8 .rodata
+ 10108 10108 400 8 %(libm)s(logf.cpp.obj):(.rodata._ZN22__llvm_libc_24_0_0_git25common_constants_internal5LOG_RE)
+ 10108 10108 400 1 __llvm_libc_24_0_0_git::common_constants_internal::LOG_R
+ 20908 20908 5f8 4 .text
+ 20908 20908 1c 2 p.o:(.text.__libc_size_probe_entry)
+ 20909 20909 1c 1 __libc_size_probe_entry
+ 20924 20924 1f4 2 %(libm)s(logf.cpp.obj):(.text.logf)
+ 20925 20925 1f4 1 logf
+ 20b18 20b18 3e8 4 %(builtins)s(adddf3.S.obj):(.text)
+ 20b19 20b19 3e8 1 __aeabi_dadd
+ 31720 31720 8 4 .bss
+ 31720 31720 8 4 p.o:(.bss.__libc_size_probe_float)
+ 0 0 ea 1 .comment
+ 0 0 ea 1 <internal>:(.comment)
+""" % {
+ "libm": ARM_LIBM,
+ "builtins": BUILTINS,
+}
+
+READELF_LOGF_ARM = """\
+There are 7 section headers, starting at offset 0x2110:
+
+Section Headers:
+ [Nr] Name Type Address Off Size ES Flg Lk Inf Al
+ [ 0] NULL 00000000 000000 000000 00 0 0 0
+ [ 1] .ARM.exidx ARM_EXIDX 000100f4 0000f4 000010 00 AL 3 0 4
+ [ 2] .rodata PROGBITS 00010108 000108 000400 00 A 0 0 8
+ [ 3] .text PROGBITS 00020908 000908 0005f8 00 AX 0 0 4
+ [ 4] .bss NOBITS 00031720 001720 000008 00 WA 0 0 4
+ [ 5] .comment PROGBITS 00000000 001720 0000ea 01 MS 0 0 1
+ [ 6] .ARM.attributes ARM_ATTRIBUTES 00000000 00180a 000041 00 0 0 1
+Key to Flags:
+ W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
+"""
+
+
+class ProbeTest(unittest.TestCase):
+ def test_parse_probe_list(self):
+ protos, probes = lsc.parse_probe_list(
+ "# comment\nfloat logf(float);\ndouble pow(double, double); # x\n"
+ "int ilogbf(float);\nlogf+pow\n"
+ )
+ self.assertEqual(sorted(protos), ["ilogbf", "logf", "pow"])
+ self.assertEqual(protos["pow"].args, ["double", "double"])
+ self.assertEqual(
+ probes,
+ [
+ ("logf", ["logf"]),
+ ("pow", ["pow"]),
+ ("ilogbf", ["ilogbf"]),
+ ("logf+pow", ["logf", "pow"]),
+ ],
+ )
+ with self.assertRaises(ValueError):
+ lsc.parse_probe_list("float logf(float);\nlogf+expf\n")
+ with self.assertRaises(ValueError):
+ lsc.parse_probe_list("void sincosf(float, float *, float *);\n")
+
+ def test_generate_probe_source(self):
+ protos, _ = lsc.parse_probe_list(
+ "float logf(float);\ndouble pow(double, double);\n"
+ )
+ src = lsc.generate_probe_source(["logf", "pow"], protos)
+ self.assertIn("float logf(float);", src)
+ self.assertIn("double pow(double, double);", src)
+ self.assertIn(
+ "__attribute__((used, retain)) volatile long double "
+ "__libc_size_probe_long_double;",
+ src,
+ )
+ self.assertIn("__libc_size_probe_float = logf(__libc_size_probe_float);", src)
+ self.assertIn(
+ "__libc_size_probe_double = pow(__libc_size_probe_double, "
+ "__libc_size_probe_double);",
+ src,
+ )
+ self.assertIn("__attribute__((weak)) int *__llvm_libc_errno(void)", src)
+ self.assertIn("__attribute__((used, retain)) __attribute__((weak))", src)
+ baseline = lsc.generate_probe_source([], protos)
+ self.assertNotIn("logf", baseline)
+ self.assertIn("void __libc_size_probe_entry(void) {\n}", baseline)
+
+ def test_parse_readelf_sections(self):
+ sections = lsc.parse_readelf_sections(READELF_LOGF_ARM)
+ by_name = {s.name: s for s in sections}
+ self.assertEqual(len(sections), 6)
+ self.assertEqual(by_name[".text"].size, 0x5F8)
+ self.assertTrue(by_name[".text"].is_flash())
+ self.assertFalse(by_name[".bss"].is_flash())
+ self.assertTrue(by_name[".bss"].is_ram())
+ self.assertFalse(by_name[".comment"].is_flash())
+ self.assertEqual(by_name[".ARM.attributes"].flags, "")
+
+ def test_parse_lld_map(self):
+ entries = lsc.parse_lld_map(MAP_LOGF_ARM)
+ self.assertEqual(entries[0], (".ARM.exidx", "<internal>:(.ARM.exidx)", 0x10))
+ self.assertEqual(len(entries), 7)
+ self.assertEqual(entries[2][0], ".text")
+ self.assertEqual(entries[2][2], 0x1C)
+
+ def test_parse_lld_map_64bit(self):
+ text = (
+ " VMA LMA Size Align Out In Symbol\n"
+ " 200158 200158 1d4 4 .text\n"
+ " 200158 200158 2c 4 "
+ "/b/libm.a(logf.cpp.o):(.text.logf)\n"
+ " 200158 200158 0 1 logf\n"
+ )
+ self.assertEqual(
+ lsc.parse_lld_map(text),
+ [(".text", "/b/libm.a(logf.cpp.o):(.text.logf)", 0x2C)],
+ )
+
+ def test_probe_metrics(self):
+ sections = lsc.parse_readelf_sections(READELF_LOGF_ARM)
+ entries = lsc.parse_lld_map(MAP_LOGF_ARM)
+ data = [lsc.Symbol("common_constants_internal::LOG_R", "x", "V", 1024)]
+ metrics = lsc.probe_metrics(sections, entries, data, [ARM_LIBM, ARM_LIBC])
+ self.assertEqual(metrics["flash"], 0x10 + 0x400 + 0x5F8)
+ self.assertEqual(metrics["ram"], 8)
+ self.assertEqual(metrics["libc_flash"], 0x400 + 0x1F4)
+ self.assertEqual(metrics["named_data"], 1024)
+ self.assertEqual(metrics["members"], ["libm.a(logf.cpp.obj)"])
+
+ baseline = {
+ "flash": 0x10 + 0x1C,
+ "ram": 8,
+ "libc_flash": 0,
+ "named_data": 0,
+ "members": [],
+ }
+ delta = lsc.probe_delta(metrics, baseline)
+ self.assertEqual(delta["flash"], 0x400 + 0x5F8 - 0x1C)
+ self.assertEqual(delta["libc_flash"], 0x400 + 0x1F4)
+ self.assertEqual(delta["other_flash"], 0x3E8) # __aeabi_dadd
+ self.assertEqual(delta["ram"], 0)
+ text = lsc.format_probe_text([("logf", delta), ("powf", {"skipped": "x"})])
+ self.assertIn("logf.cpp.obj", text)
+ self.assertIn("powf", text)
+ md = lsc.format_probe_markdown([("logf", delta)], "t")
+ self.assertIn("| logf | %d | %d |" % (delta["flash"], delta["libc_flash"]), md)
+
+
class RulesFileTest(unittest.TestCase):
"""Sanity checks of the rules files in this directory."""
diff --git a/libc/utils/size_check/probes/math.txt b/libc/utils/size_check/probes/math.txt
new file mode 100644
index 0000000000000..265d64c2a58ba
--- /dev/null
+++ b/libc/utils/size_check/probes/math.txt
@@ -0,0 +1,33 @@
+# Probes for the linked size report of the math functions.
+#
+# Each prototype adds a probe that calls the function. A line of the form
+# f+g[+...] adds a probe that calls several functions, which shows how much
+# code and data they share. Functions that are not part of the build are
+# skipped.
+
+float logf(float);
+float log2f(float);
+float log10f(float);
+float expf(float);
+float exp2f(float);
+float exp10f(float);
+float powf(float, float);
+float sinf(float);
+float cosf(float);
+float tanf(float);
+float atanf(float);
+float atan2f(float, float);
+float sqrtf(float);
+double log(double);
+double log2(double);
+double exp(double);
+double exp2(double);
+double pow(double, double);
+double sin(double);
+double cos(double);
+double atan(double);
+double atan2(double, double);
+
+logf+expf+powf
+sinf+cosf
+log+exp+pow
More information about the llvm-branch-commits
mailing list