From b8149c86f7fe7c525610ed9855846b4002d5d789 Mon Sep 17 00:00:00 2001 From: Lau Josefsen <44977457+LauJosefsen@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:39:12 +0200 Subject: [PATCH] Support GCC and Clang PGO builds with embedded profile flushing --- .gitignore | 2 + build/Makefile.global | 36 ++++++++-- build/tests/pgo.c | 40 +++++++++++ build/tests/pgo.sh | 40 +++++++++++ configure.ac | 22 ++++++ docs/source/index.rst | 1 + docs/source/miscellaneous/pgo.rst | 116 ++++++++++++++++++++++++++++++ main/main.c | 19 +++++ 8 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 build/tests/pgo.c create mode 100644 build/tests/pgo.sh create mode 100644 docs/source/miscellaneous/pgo.rst diff --git a/.gitignore b/.gitignore index b76b5a787caa..edccc5a2f6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -266,6 +266,7 @@ tmp-php.ini # ------------------------------------------------------------------------------ *.gcda *.gcno +/pgo-data/ /gcovr.xml /gcovr_html/ /lcov_html/ @@ -311,6 +312,7 @@ tmp-php.ini # Special cases to invert previous ignore patterns # ------------------------------------------------------------------------------ !/ext/bcmath/libbcmath/src/config.h +!/build/tests/*.sh !/ext/fileinfo/libmagic/config.h !/ext/fileinfo/libmagic.patch !/ext/fileinfo/magicdata.patch diff --git a/build/Makefile.global b/build/Makefile.global index 16d57b54c831..bae84788bf09 100644 --- a/build/Makefile.global +++ b/build/Makefile.global @@ -139,9 +139,30 @@ distclean: clean rm -f ext/phar/phar/phar.inc; \ fi +PGO_DATA_DIR = $(top_builddir)/pgo-data +LLVM_PROFDATA = llvm-profdata +PROF_GEN_FLAGS = $(PHP_PGO_GENERATE_FLAGS) +PROF_USE_FLAGS = $(PHP_PGO_USE_FLAGS) + prof-gen: - CCACHE_DISABLE=1 $(MAKE) PROF_FLAGS=-fprofile-generate all - find . -name \*.gcda | xargs rm -f + @test "$(PHP_PGO_COMPILER)" != unsupported || { echo "PGO requires GCC or Clang."; exit 1; } + $(MAKE) prof-clean + $(MAKE) prof-reset + CCACHE_DISABLE=1 $(MAKE) PROF_FLAGS='$(PROF_GEN_FLAGS)' all + $(MAKE) prof-reset + +prof-reset: + find . -name \*.gcda -type f -exec rm -f {} + + mkdir -p "$(PGO_DATA_DIR)" + find "$(PGO_DATA_DIR)" -name \*.profraw -type f -exec rm -f {} + + rm -f "$(PGO_DATA_DIR)/php.profdata" "$(PGO_DATA_DIR)/profiles.list" + +prof-merge: + @test "$(PHP_PGO_COMPILER)" = clang || { echo "prof-merge requires a Clang build."; exit 1; } + mkdir -p "$(PGO_DATA_DIR)" + find "$(PGO_DATA_DIR)" -name \*.profraw -type f -print > "$(PGO_DATA_DIR)/profiles.list" + @test -s "$(PGO_DATA_DIR)/profiles.list" || { echo "No Clang profiles found; train the instrumented build first."; exit 1; } + "$(LLVM_PROFDATA)" merge --input-files="$(PGO_DATA_DIR)/profiles.list" --output="$(PGO_DATA_DIR)/php.profdata" prof-clean: find . -name \*.lo -o -name \*.o | xargs rm -f @@ -150,7 +171,14 @@ prof-clean: rm -f libphp.la $(SAPI_CLI_PATH) $(SAPI_CGI_PATH) $(SAPI_LITESPEED_PATH) $(SAPI_FPM_PATH) $(SAPI_PHPDBG_PATH) $(SAPI_PHPDBG_SHARED_PATH) $(OVERALL_TARGET) modules/* libs/* prof-use: - CCACHE_DISABLE=1 $(MAKE) PROF_FLAGS=-fprofile-use all + @test "$(PHP_PGO_COMPILER)" != unsupported || { echo "PGO requires GCC or Clang."; exit 1; } + @if test "$(PHP_PGO_COMPILER)" = clang; then \ + $(MAKE) prof-merge; \ + else \ + test -n "`find . -name \*.gcda -type f -print -quit`" || { echo "No GCC profiles found; train the instrumented build first."; exit 1; }; \ + fi + $(MAKE) prof-clean + CCACHE_DISABLE=1 $(MAKE) PROF_FLAGS='$(PROF_USE_FLAGS)' all %_arginfo.h: %.stub.php @if test -e "$(top_srcdir)/build/gen_stub.php"; then \ @@ -163,4 +191,4 @@ prof-use: fi; \ fi; -.PHONY: all clean install distclean test prof-gen prof-clean prof-use +.PHONY: all clean install distclean test prof-gen prof-reset prof-merge prof-clean prof-use diff --git a/build/tests/pgo.c b/build/tests/pgo.c new file mode 100644 index 000000000000..0fe78811a953 --- /dev/null +++ b/build/tests/pgo.c @@ -0,0 +1,40 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + +----------------------------------------------------------------------+ +*/ + +#include "sapi/embed/php_embed.h" +#include +#include + +int main(int argc, char **argv) +{ + php_embed_module.php_ini_ignore = 1; + if (php_embed_init(argc, argv) == FAILURE) { + return 1; + } + + int status = 0; + zend_first_try { + if (zend_eval_string( + "$rows = [];" + "for ($i = 0; $i < 1000; $i++) { $rows[] = ['id' => $i, 'value' => $i * 3]; }" + "$sum = array_sum(array_column($rows, 'value'));" + "echo json_encode(['sum' => $sum, 'count' => count($rows)]), PHP_EOL;", + NULL, "PGO smoke test") == FAILURE) { + status = 1; + } + } zend_catch { + status = 1; + } zend_end_try(); + + php_embed_shutdown(); + fflush(stdout); + _Exit(status); +} diff --git a/build/tests/pgo.sh b/build/tests/pgo.sh new file mode 100644 index 000000000000..2ed75ad99b4b --- /dev/null +++ b/build/tests/pgo.sh @@ -0,0 +1,40 @@ +#!/bin/sh +set -eu + +srcdir=$(CDPATH= cd -- "${1:?Usage: pgo.sh /path/to/php-src (from a configured embed build)}" && pwd) +builddir=$(pwd) +test -f Makefile || { echo "Run this test from a configured build directory." >&2; exit 1; } +test -f "$srcdir/build/tests/pgo.c" +test -f libs/libphp.so || { echo "Build PHP with --enable-embed first." >&2; exit 1; } + +make_pgo() { + "${MAKE:-make}" LLVM_PROFDATA="${LLVM_PROFDATA:-llvm-profdata}" "$@" +} + +cc=$(sed -n 's/^CC = //p' Makefile) +test -n "$cc" +$cc -I"$builddir" -I"$builddir/main" -I"$builddir/Zend" \ + -I"$srcdir" -I"$srcdir/main" -I"$srcdir/Zend" -I"$srcdir/TSRM" \ + "$srcdir/build/tests/pgo.c" -L"$builddir/libs" -Wl,-rpath,"$builddir/libs" \ + -lphp -o pgo-host + +expected='{"sum":1498500,"count":1000}' +test "$(./pgo-host)" = "$expected" +make_pgo prof-reset +before=$(cksum sapi/cli/php) +if make_pgo prof-use > pgo-missing-profile.log 2>&1; then + echo "prof-use accepted an untrained build." >&2 + exit 1 +fi +test "$(cksum sapi/cli/php)" = "$before" + +make_pgo prof-gen +test -z "$(find . -name '*.gcda' -o -name '*.profraw')" +test "$(./pgo-host)" = "$expected" +test -n "$(find . -name '*.gcda' -o -name '*.profraw')" + +make_pgo prof-use +make_pgo prof-reset +test "$(./pgo-host)" = "$expected" +test -z "$(find . -name '*.gcda' -o -name '*.profraw')" +echo "PGO build transitions and embedded profile flushing passed." diff --git a/configure.ac b/configure.ac index d4842a28467b..520e36ca58a4 100644 --- a/configure.ac +++ b/configure.ac @@ -122,6 +122,28 @@ AC_PROG_CPP AC_USE_SYSTEM_EXTENSIONS AC_PROG_LN_S +AC_CACHE_CHECK([whether the C compiler is Clang], [php_cv_pgo_clang], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ +#ifndef __clang__ +# error Not Clang +#endif + ]], [])], [php_cv_pgo_clang=yes], [php_cv_pgo_clang=no])]) + +AS_VAR_IF([php_cv_pgo_clang], [yes], [ + PHP_PGO_COMPILER=clang + PHP_PGO_GENERATE_FLAGS='-fprofile-generate=$(PGO_DATA_DIR) -fprofile-update=atomic -DPHP_PGO_GENERATE' + PHP_PGO_USE_FLAGS='-fprofile-use=$(PGO_DATA_DIR)/php.profdata -DPHP_PGO_USE' +], [ + AS_VAR_IF([GCC], [yes], [ + PHP_PGO_COMPILER=gcc + PHP_PGO_GENERATE_FLAGS='-fprofile-generate -fprofile-update=atomic -DPHP_PGO_GENERATE' + PHP_PGO_USE_FLAGS='-fprofile-use -fprofile-correction -DPHP_PGO_USE' + ], [PHP_PGO_COMPILER=unsupported]) +]) +PHP_SUBST([PHP_PGO_COMPILER]) +PHP_SUBST([PHP_PGO_GENERATE_FLAGS]) +PHP_SUBST([PHP_PGO_USE_FLAGS]) + AS_VAR_IF([cross_compiling], [yes], [AC_CHECK_PROGS([BUILD_CC], [gcc clang c99 c89 cc cl], [none]) AC_MSG_CHECKING([for native build C compiler]) diff --git a/docs/source/index.rst b/docs/source/index.rst index 21e2526f47f6..3c89d4180f96 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,6 +22,7 @@ miscellaneous/stubs miscellaneous/writing-tests miscellaneous/running-tests + miscellaneous/pgo Welcome to the php-src documentation! diff --git a/docs/source/miscellaneous/pgo.rst b/docs/source/miscellaneous/pgo.rst new file mode 100644 index 000000000000..844f0590ca8c --- /dev/null +++ b/docs/source/miscellaneous/pgo.rst @@ -0,0 +1,116 @@ +############################# + Profile-guided optimization +############################# + +Profile-guided optimization (PGO) uses measurements from an instrumented PHP build to optimize a +second build. The ``prof-gen`` and ``prof-use`` make targets support GCC and Clang. These targets +are optional; a normal ``make`` does not enable PGO. + +Use the same source tree, compiler, architecture, configure options and optimization flags for +both phases. Keep the build directory in place while collecting profiles and rebuilding. GCC +records counters beside the object files; Clang records raw profiles in ``pgo-data/`` under the +build directory. + +************************** + Build, train and rebuild +************************** + +Configure PHP with the features required by the training applications. For example: + +.. code:: shell + + ./buildconf --force + ./configure --enable-zts --enable-embed + make -j8 prof-gen + +``prof-gen`` removes old objects and profiles, builds an instrumented interpreter with atomic +counter updates, then removes profiles produced by build-time tools. Run the instrumented PHP +binary or embedded library against the training applications before rebuilding: + +.. code:: shell + + sapi/cli/php -d opcache.jit=disable -d opcache.jit_buffer_size=0 training.php + make -j8 prof-use + +``prof-use`` checks for training profiles before removing the instrumented objects. It then +builds an optimized interpreter without instrumentation. Training applications and configurations +must also keep JIT disabled when comparing interpreter build optimizations. + +For Clang, configure with the chosen compiler and select the corresponding ``llvm-profdata`` +executable if it is not named ``llvm-profdata``: + +.. code:: shell + + CC=clang ./configure --enable-zts --enable-embed + make -j8 prof-gen + sapi/cli/php -d opcache.jit=disable -d opcache.jit_buffer_size=0 training.php + make -j8 prof-use LLVM_PROFDATA=llvm-profdata + +``prof-use`` automatically merges the raw Clang profiles into ``pgo-data/php.profdata``. To merge +and inspect them before rebuilding: + +.. code:: shell + + make prof-merge LLVM_PROFDATA=llvm-profdata + llvm-profdata show --all-functions --counts pgo-data/php.profdata + +``make prof-clean`` removes compiled objects and binaries while preserving training data. +``make prof-reset`` removes GCC counters, raw Clang profiles and the merged Clang profile while +preserving compiled objects. ``make prof-gen`` starts a new training cycle and discards the +previous cycle's profiles. + +The make variables ``PROF_GEN_FLAGS`` and ``PROF_USE_FLAGS`` allow custom compiler profiling +flags. Retain ``-DPHP_PGO_GENERATE`` in generation flags to flush profiles at PHP module shutdown, +and ``-DPHP_PGO_USE`` in use flags to keep the shutdown path excluded from profiling in both phases. +``PGO_DATA_DIR`` controls the Clang profile directory; use the same value for generation, +training, merging and profile use. If ``LLVM_PROFILE_FILE`` is set during Clang training, keep its +output in that directory so that ``prof-merge`` can discover the profiles. + +************************* + Representative training +************************* + +Training should cover the work performed by the applications that will use the optimized build. +Include application startup, persistent workers, fresh application instances, CLI jobs, +validation, serialization, template rendering, container resolution and instrumentation as +appropriate. Exercise successful and failing inputs, common and infrequent branches, and the +native extensions required by the applications. Train each SAPI that will be deployed. + +Choose the relative frequency of these operations to reflect the expected application mix. +Separate training inputs from evaluation inputs, and include workloads absent from training to +check whether the gains generalize. A small smoke test checks the build machinery; it is not a +representative performance training workload. + +Inspect profile coverage and compiler diagnostics. A profile generated by a different source +revision or compiler can be incompatible even when its files exist. Regenerate profiles when +changing PHP, the compiler, architecture, extensions, configure options or optimization flags. + +**************** + Embedded hosts +**************** + +Instrumented builds flush counters during ``php_module_shutdown()``. An embedded host must call +PHP's shutdown routines after training and before terminating. This also writes profiles when +the host exits without invoking the C runtime's exit handlers. Atomic counters support training +with multiple PHP threads; the host must stop those threads before shutting down PHP. + +Only instrumented builds contain this flushing hook. The optimized build does not add a +userland function, and the hook is absent from normal builds. + +*************************** + Build workflow smoke test +*************************** + +The Linux shared-embed smoke test verifies that an untrained ``prof-use`` fails without replacing +the current interpreter, that the generation and use phases rebuild objects, and that an +embedded process writes profiles before calling ``_Exit()``. It also checks identical output and +the absence of new profiles after an optimized run. + +From a fresh build configured with ``--enable-embed`` and with ``make`` completed: + +.. code:: shell + + MAKEFLAGS=-j8 sh /path/to/php-src/build/tests/pgo.sh /path/to/php-src + +For Clang, set ``LLVM_PROFDATA`` to the matching executable if necessary. The test replaces the +current build with an instrumented build and then a profile-use build, and clears profiling data. diff --git a/main/main.c b/main/main.c index ad7ae8a5177e..858754a2174d 100644 --- a/main/main.c +++ b/main/main.c @@ -91,6 +91,14 @@ #include "main_arginfo.h" /* }}} */ +#ifdef PHP_PGO_GENERATE +# ifdef __clang__ +extern int __llvm_profile_dump(void); +# else +extern void __gcov_dump(void); +# endif +#endif + PHPAPI int (*php_register_internal_extensions_func)(void) = php_register_internal_extensions; #ifndef ZTS @@ -2488,6 +2496,9 @@ int php_module_shutdown_wrapper(sapi_module_struct *sapi_globals) /* }}} */ /* {{{ php_module_shutdown */ +#if defined(PHP_PGO_GENERATE) || defined(PHP_PGO_USE) +__attribute__((no_profile_instrument_function)) +#endif void php_module_shutdown(void) { int module_number=0; @@ -2563,6 +2574,14 @@ void php_module_shutdown(void) #endif zend_observer_shutdown(); + +#ifdef PHP_PGO_GENERATE +# ifdef __clang__ + __llvm_profile_dump(); +# else + __gcov_dump(); +# endif +#endif } /* }}} */