From 2f05a4d6e219bbec3e807496edfe0ee01ad0d539 Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Thu, 14 May 2026 00:21:21 -0700 Subject: [PATCH] Add PGO to Windows builds --- .github/workflows/publish.yml | 8 ++++++ CMakeLists.txt | 11 +++++++-- pyproject.toml | 2 +- setup.py | 46 +++++++++++++++++++++++++---------- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e314b3b1c..947447ddc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,6 +44,14 @@ jobs: echo "/usr/local/opt/llvm/bin" >> $GITHUB_PATH fi + # Windows builds may use Clang-CL which needs llvm-profdata for PGO. MSVC + # PGO does not require LLVM. + - name: Install LLVM (Windows) + if: runner.os == 'Windows' + run: | + choco install llvm -y + echo "C:\Program Files\LLVM\bin" >> $env:GITHUB_PATH + - name: Install cibuildwheel run: python -m pip install cibuildwheel diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ffac1281..65270a5a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,8 +112,6 @@ if(ENABLE_LTO) set(LTO_FLAG "-flto") set(LTO_LINKER_FLAGS "-flto -fuse-linker-plugin -ffat-lto-objects") elseif(USING_MSVC) - # MSVC uses /GL for compilation and /LTCG for linking - # Alternatively, we can use CMake's INTERPROCEDURAL_OPTIMIZATION set(LTO_FLAG "/GL") set(LTO_LINKER_FLAGS "/LTCG") endif() @@ -139,6 +137,10 @@ if(ENABLE_PGO_GENERATE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-generate") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-generate") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fprofile-generate") + elseif(MSVC) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /GENPROFILE") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /GENPROFILE") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /GENPROFILE") else() message(WARNING "PGO profile generation is not supported with compiler: ${CMAKE_CXX_COMPILER_ID}") endif() @@ -160,6 +162,11 @@ if(ENABLE_PGO_USE) message(STATUS "PGO: Using GCC profile data from build directory") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-use -fprofile-correction") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-use -fprofile-correction") + elseif(MSVC) + message(STATUS "PGO: Using MSVC profile data from build directory") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /USEPROFILE") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /USEPROFILE") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /USEPROFILE") else() message(WARNING "PGO profile use is not supported with compiler: ${CMAKE_CXX_COMPILER_ID}") endif() diff --git a/pyproject.toml b/pyproject.toml index 28211e1e5..99a3ac209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,4 +62,4 @@ archs = ["arm64"] environment = { CINDERX_ENABLE_PGO = "1", CINDERX_ENABLE_LTO = "1" } [tool.cibuildwheel.windows] -environment = { CINDERX_ENABLE_LTO = "1" } +environment = { CINDERX_ENABLE_PGO = "1", CINDERX_ENABLE_LTO = "1" } diff --git a/setup.py b/setup.py index 94d2d66f2..31f3afaa4 100644 --- a/setup.py +++ b/setup.py @@ -66,13 +66,14 @@ def compute_package_version() -> str: @lru_cache(maxsize=1) -def get_compiler() -> tuple[str, str]: +def get_compiler() -> tuple[str | None, str | None]: """ Prefers GCC if a new enough version is installed as this is what the cibuildwheel environment uses. Returns: - A tuple of (c_compiler, cxx_compiler) paths. + A tuple of (c_compiler, cxx_compiler) paths, or (None, None) to use + CMake's default compiler detection. """ cc_path = os.environ.get("CC") cxx_path = os.environ.get("CXX") @@ -124,6 +125,12 @@ def get_compiler() -> tuple[str, str]: print(f"Using Clang: {clang_path}, {clangxx_path}") return (clang_path, clangxx_path) + # On Windows, if no compiler is explicitly found, let CMake use its default + # (which will be MSVC with the appropriate generator) + if platform.system() == "Windows": + print("No explicit compiler found, using CMake default (MSVC on Windows)") + return (None, None) + raise RuntimeError("Cannot find suitable C/C++ compiler (tried gcc and clang)") @@ -160,7 +167,9 @@ def print_section(title: str) -> None: print(separator) cc, _ = get_compiler() - is_clang = "clang" in cc + is_clang = cc and "clang" in cc + is_windows = platform.system() == "Windows" + is_msvc = is_windows and not is_clang print_section("PGO STAGE 1/3: Building with profile generation instrumentation") @@ -220,7 +229,7 @@ def main(): } if is_clang: workload_args["cwd"] = clang_pgo_dir - subprocess.run(workload_cmd, **workload_args) + subprocess.run(workload_cmd, **workload_args) if is_clang: print_section("PGO STAGE 2b: Merging profile data") @@ -244,6 +253,9 @@ def main(): ] + profraw_files subprocess.run(merge_cmd, check=True) print(f"Merged profile written to {clang_merged_profile}") + elif is_msvc: + print_section("PGO STAGE 2b: MSVC profile data") + print("MSVC automatically merges .pgc files into a .pgd database") print_section("PGO STAGE 3/3: Rebuilding with profile-guided optimizations") @@ -264,20 +276,22 @@ def main(): cmake_files = os.path.join(self.build_temp, "CMakeFiles") if os.path.exists(cmake_files): - print(f" Cleaning {cmake_files} (preserving .gcda/.gcno for GCC PGO)") + print( + f" Cleaning {cmake_files} (preserving .gcda/.gcno for GCC PGO, .pgc/.pgd for MSVC PGO)" + ) for root, _dirs, files in os.walk(cmake_files, topdown=False): for f in files: file_path = os.path.join(root, f) - # Preserve GCC PGO profiling files - if f.endswith((".gcda", ".gcno")): + # Preserve PGO profiling files. + if f.endswith((".gcda", ".gcno", ".pgc", ".pgd")): continue os.remove(file_path) - # Remove empty directories, but only if they don't contain - # preserved files (the topdown=False walk handles this) + # Remove empty directories, but only if they don't contain + # preserved files (the topdown=False walk handles this) try: os.rmdir(root) # Only removes if empty except OSError: - pass # Directory not empty, contains .gcda/.gcno files + pass # Directory not empty, contains preserved files # Remove all object files and libraries to force rebuild for rm_root in (self.build_temp, self.build_lib): @@ -287,7 +301,9 @@ def main(): if "pgo_data" in root: continue for f in files: - if f.endswith((".o", ".so", ".a")): + if f.endswith( + (".o", ".so", ".a", ".obj", ".lib", ".pyd", ".dll", ".exp") + ): file_path = os.path.join(root, f) print(f" Removing {file_path}") os.remove(file_path) @@ -391,11 +407,15 @@ def _run_cmake(self, extension: CMakeExtension) -> None: cmake_args += [ f"-DCMAKE_BUILD_TYPE={build_type}", f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={ext_dir}", - f"-DCMAKE_C_COMPILER={cc}", - f"-DCMAKE_CXX_COMPILER={cxx}", f"-DCMAKE_VERBOSE_MAKEFILE:BOOL={verbose_makefile}", ] + # Only pass explicit compiler if specified (None means use CMake defaults). + if cc: + cmake_args.append(f"-DCMAKE_C_COMPILER={cc}") + if cxx: + cmake_args.append(f"-DCMAKE_CXX_COMPILER={cxx}") + if self.cinderx_pgo_stage == PgoStage.GENERATE: cmake_args.append("-DENABLE_PGO_GENERATE=ON") cmake_args.append("-DENABLE_PGO_USE=OFF")