Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ set(CTEST_TEST_TIMEOUT 90 CACHE STRING "Per-test timeout (s) for CTest")
FetchContent_MakeAvailable(Catch2 GSL mimicpp)

include(Modules/private/CreateCoverageTargets.cmake)
include(Modules/private/PhlexSymbolVisibility.cmake)
include(Modules/private/PhlexOptimization.cmake)

option(ENABLE_TSAN "Enable Thread Sanitizer" OFF)
option(ENABLE_ASAN "Enable Address Sanitizer" OFF)
Expand Down
164 changes: 164 additions & 0 deletions Modules/private/PhlexOptimization.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Provides phlex_apply_optimizations(target), which applies safe,
# performance-oriented compiler flags to a Phlex shared library target.
# Also defines the PHLEX_HIDE_SYMBOLS and PHLEX_ENABLE_IPO options and
# documents how they interact.
#
# Two flags are applied (subject to compiler support and platform):
#
# -fno-semantic-interposition (GCC >= 9, Clang >= 8)
# The compiler may assume that exported symbols in this shared library are
# not overridden at runtime by LD_PRELOAD or another DSO. This is the
# natural complement of -fvisibility=hidden: once the exported-symbol
# surface is bounded by explicit export macros, treating those symbols as
# non-interposable allows the compiler to inline, devirtualise, and
# generate direct (non-PLT) calls for same-DSO accesses to exported
# functions.
#
# External plugins continue to call Phlex symbols through the standard
# PLT/GOT mechanism; only code compiled as part of a Phlex shared library
# itself benefits.
#
# Applied only when PHLEX_HIDE_SYMBOLS=ON (export macros are present and
# the exported-symbol set is well-defined).
#
# -fno-plt (GCC >= 7.3, Clang >= 4, ELF platforms)
# Calls FROM a Phlex library TO symbols in other shared libraries (TBB,
# Boost, spdlog, ...) bypass the PLT stub and load the target address
# directly from the GOT. This replaces one level of indirection on every
# cross-DSO call after first resolution. Semantics are unchanged; only
# the dispatch mechanism differs.
#
# Not applied on Apple platforms: Mach-O uses two-level namespaces and the
# PLT abstraction does not map directly to ELF semantics.
#
# Intentionally excluded:
# -ffast-math / -funsafe-math-optimizations
# Physics and numerical code relies on well-defined floating-point
# semantics (NaN propagation, exact rounding, signed-zero behaviour).
# These flags may silently produce incorrect numerical results and are
# therefore not enabled.
#
# Options and their interaction with CMAKE_BUILD_TYPE
#
# Both PHLEX_HIDE_SYMBOLS and PHLEX_ENABLE_IPO are defined here so that their
# defaults can be derived together. The effective defaults are:
#
# CMAKE_BUILD_TYPE PHLEX_ENABLE_IPO PHLEX_HIDE_SYMBOLS
# ───────────────── ─────────────── ─────────────────
# Release ON ON
# RelWithDebInfo ON ON
# Debug / sanitizer OFF OFF
# not set OFF OFF
#
# Both options can be overridden independently on the command line:
#
# -DPHLEX_HIDE_SYMBOLS=ON -DPHLEX_ENABLE_IPO=ON → LTO + -fno-semantic-interposition
# (maximum optimization)
# -DPHLEX_HIDE_SYMBOLS=OFF -DPHLEX_ENABLE_IPO=ON → LTO only; -fno-semantic-interposition
# is NOT applied (valid, useful for
# benchmarking against the ON case)
# -DPHLEX_HIDE_SYMBOLS=ON -DPHLEX_ENABLE_IPO=OFF → -fno-semantic-interposition only
# -DPHLEX_HIDE_SYMBOLS=OFF -DPHLEX_ENABLE_IPO=OFF → no special optimization flags
#
# The per-target flags in phlex_apply_optimizations() self-adjust automatically
# to reflect whichever combination is in effect.
#
# PHLEX_ENABLE_IPO (default ON for Release/RelWithDebInfo, OFF otherwise)
# When ON, enables interprocedural optimization (LTO) for Release and
# RelWithDebInfo configurations. LTO is safe with or without symbol hiding
# because export attributes preserve the complete exported-symbol set.
# External plugins compiled without LTO link against the normal
# exported-symbol table and are unaffected.
#
# PHLEX_HIDE_SYMBOLS (default ON for Release/RelWithDebInfo, OFF otherwise)
# When ON: hidden-by-default visibility; export macros mark the public API.
# When OFF: all symbols visible; _internal targets become thin INTERFACE
# aliases of their public counterparts.

include_guard()

include(CheckCXXCompilerFlag)

# Probe flag availability once at module-load time (results are cached in the
# CMake cache and reused across reconfigures).
check_cxx_compiler_flag("-fno-semantic-interposition" PHLEX_CXX_HAVE_NO_SEMANTIC_INTERPOSITION)

if(NOT APPLE)
check_cxx_compiler_flag("-fno-plt" PHLEX_CXX_HAVE_NO_PLT)
endif()

# ---------------------------------------------------------------------------
# Interprocedural optimization (LTO) — defined first so its value can inform
# the PHLEX_HIDE_SYMBOLS default below.
# ---------------------------------------------------------------------------
cmake_policy(SET CMP0069 NEW)
include(CheckIPOSupported)

if(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo)$")
set(_phlex_ipo_default ON)
else()
set(_phlex_ipo_default OFF)
endif()

option(
PHLEX_ENABLE_IPO
[=[Enable interprocedural optimization (LTO) for Release and RelWithDebInfo
builds. Defaults to ON when CMAKE_BUILD_TYPE is Release or RelWithDebInfo.
Can be combined with PHLEX_HIDE_SYMBOLS=ON for maximum optimization, or with
PHLEX_HIDE_SYMBOLS=OFF to benchmark LTO benefit without symbol hiding.]=]
"${_phlex_ipo_default}"
)

# ---------------------------------------------------------------------------
# Symbol hiding — default follows CMAKE_BUILD_TYPE independently of IPO.
# The two options are orthogonal: both ON/OFF combinations are valid and
# produce different optimization profiles for benchmarking.
# ---------------------------------------------------------------------------
if(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo)$")
set(_phlex_hide_default ON)
else()
set(_phlex_hide_default OFF)
endif()

option(
PHLEX_HIDE_SYMBOLS
[=[Hide non-exported symbols in shared libraries (ON = curated API with
hidden-by-default visibility; OFF = all symbols visible). Defaults to ON for
Release/RelWithDebInfo builds.
Combined with PHLEX_ENABLE_IPO=ON: -fno-semantic-interposition is also applied
for maximum optimization. Setting OFF while PHLEX_ENABLE_IPO=ON is valid and
useful for comparing LTO performance with and without symbol hiding.]=]
Comment on lines +128 to +130
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text for PHLEX_HIDE_SYMBOLS says -fno-semantic-interposition is applied when combined with PHLEX_ENABLE_IPO=ON, but phlex_apply_optimizations() applies -fno-semantic-interposition whenever PHLEX_HIDE_SYMBOLS is ON (independent of IPO). Please update the option description (or gate the flag on PHLEX_ENABLE_IPO) so the documented behavior matches the implementation.

Suggested change
Combined with PHLEX_ENABLE_IPO=ON: -fno-semantic-interposition is also applied
for maximum optimization. Setting OFF while PHLEX_ENABLE_IPO=ON is valid and
useful for comparing LTO performance with and without symbol hiding.]=]
When ON and supported by the compiler, -fno-semantic-interposition is also
applied for additional optimization. This behavior is independent of
PHLEX_ENABLE_IPO; both options can be toggled separately to compare profiles.]=]

Copilot uses AI. Check for mistakes.
"${_phlex_hide_default}"
)

# ---------------------------------------------------------------------------
# Activate LTO (if enabled and supported)
# ---------------------------------------------------------------------------
if(PHLEX_ENABLE_IPO)
check_ipo_supported(RESULT _phlex_ipo_supported OUTPUT _phlex_ipo_output LANGUAGES CXX)
if(_phlex_ipo_supported)
# Set defaults for all targets created in this scope and below. The
# *_RELEASE and *_RELWITHDEBINFO variants leave Debug/Coverage/sanitizer
# builds unaffected (those configs override optimization independently).
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
message(STATUS "Phlex: LTO enabled for Release and RelWithDebInfo builds")
else()
message(WARNING "Phlex: PHLEX_ENABLE_IPO=ON but LTO is not supported: ${_phlex_ipo_output}")
endif()
endif()

# ---------------------------------------------------------------------------
function(phlex_apply_optimizations target)
# -fno-semantic-interposition pairs with PHLEX_HIDE_SYMBOLS: the compiler
# may only treat exported symbols as non-interposable once the exported-
# symbol surface has been explicitly bounded by export macros.
if(PHLEX_HIDE_SYMBOLS AND PHLEX_CXX_HAVE_NO_SEMANTIC_INTERPOSITION)
target_compile_options("${target}" PRIVATE "-fno-semantic-interposition")
endif()

# -fno-plt reduces cross-DSO call overhead on ELF (Linux) platforms.
if(PHLEX_CXX_HAVE_NO_PLT)
target_compile_options("${target}" PRIVATE "-fno-plt")
endif()
endfunction()
112 changes: 112 additions & 0 deletions Modules/private/PhlexSymbolVisibility.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
include(GenerateExportHeader)

function(phlex_apply_symbol_visibility target)
set(EXPORT_HEADER "${PROJECT_BINARY_DIR}/include/phlex/${target}_export.hpp")
set(EXPORT_MACRO_NAME "${target}_EXPORT")

generate_export_header(
${target}
BASE_NAME ${target}
EXPORT_FILE_NAME ${EXPORT_HEADER}
EXPORT_MACRO_NAME ${EXPORT_MACRO_NAME}
STATIC_DEFINE "${target}_STATIC_DEFINE"
)

if(PHLEX_HIDE_SYMBOLS)
set_target_properties(
${target}
PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON
)
endif()

target_include_directories(${target} PUBLIC $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>)

install(FILES "${EXPORT_HEADER}" DESTINATION include/phlex)
endfunction()

# Create a companion library <target>_internal:
#
# When PHLEX_HIDE_SYMBOLS is ON (default): a non-installed shared library with
# default (visible) symbol visibility, compiled from the same sources as
# <target>. This allows tests to access non-exported implementation details
# without requiring every internal symbol to carry an EXPORT macro, and enables
# before/after comparison of library/executable sizes and link/load times.
#
# When PHLEX_HIDE_SYMBOLS is OFF: an INTERFACE target that simply links to
# <target>. Since all public library symbols are already visible in this mode,
# no separate compilation is needed and _internal targets are effectively
# identical to their public counterparts.
#
# Usage (in the same CMakeLists.txt that defines <target>):
# phlex_make_internal_library(<target> LIBRARIES [PUBLIC ...] [PRIVATE ...])
#
# The LIBRARIES arguments mirror those of the original cet_make_library call but
# may substitute other _internal targets for the corresponding public ones so
# that the full transitive symbol set is visible (PHLEX_HIDE_SYMBOLS=ON only).
function(phlex_make_internal_library target)
cmake_parse_arguments(ARG "" "" "LIBRARIES" ${ARGN})

set(internal "${target}_internal")

if(NOT PHLEX_HIDE_SYMBOLS)
# All public symbols already visible — _internal is a thin INTERFACE wrapper.
add_library(${internal} INTERFACE)
target_link_libraries(${internal} INTERFACE ${target})
return()
endif()

# Retrieve sources and source directory from the public target so we don't
# have to maintain a separate source list.
get_target_property(srcs ${target} SOURCES)
if(NOT srcs)
message(FATAL_ERROR "phlex_make_internal_library: ${target} has no SOURCES property")
endif()
get_target_property(src_dir ${target} SOURCE_DIR)
get_target_property(bin_dir ${target} BINARY_DIR)

# Convert relative paths to absolute. Generated sources (e.g. configure_file
# output) live in the binary directory rather than the source directory.
set(abs_srcs "")
foreach(s IN LISTS srcs)
if(IS_ABSOLUTE "${s}")
list(APPEND abs_srcs "${s}")
elseif(EXISTS "${src_dir}/${s}")
list(APPEND abs_srcs "${src_dir}/${s}")
else()
list(APPEND abs_srcs "${bin_dir}/${s}")
endif()
endforeach()

# Use add_library directly (not cet_make_library) so that cetmodules does not
# register this target for installation or package export.
add_library(${internal} SHARED ${abs_srcs})

if(ARG_LIBRARIES)
target_link_libraries(${internal} ${ARG_LIBRARIES})
endif()

# Cetmodules automatically adds $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}> for
# libraries it manages; replicate that here so consumers (e.g. layer_generator_internal)
# can resolve project headers such as #include "phlex/core/...".
# The _export.hpp headers live in PROJECT_BINARY_DIR/include/phlex.
# Without CXX_VISIBILITY_PRESET hidden the export macros expand to the default
# visibility attribute, making every symbol visible — exactly what we want here.
target_include_directories(
${internal}
PUBLIC
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>"
"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>"
)

# Propagate compile definitions and options that the public target carries
# (e.g. BOOST_DLL_USE_STD_FS for run_phlex) so the internal build is equivalent.
get_target_property(defs ${target} COMPILE_DEFINITIONS)
if(defs)
target_compile_definitions(${internal} PRIVATE ${defs})
endif()

get_target_property(opts ${target} COMPILE_OPTIONS)
if(opts)
target_compile_options(${internal} PRIVATE ${opts})
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phlex_make_internal_library() copies the public target's COMPILE_OPTIONS onto the _internal target. When PHLEX_HIDE_SYMBOLS is ON, that includes -fno-semantic-interposition from phlex_apply_optimizations(), but the _internal library is explicitly built with full/default visibility (all symbols interposable). This undermines the safety assumptions documented for -fno-semantic-interposition and can make internal-vs-public benchmarking inaccurate. Filter out -fno-semantic-interposition for _internal targets (or avoid copying optimization flags wholesale and re-apply only the safe subset).

Suggested change
target_compile_options(${internal} PRIVATE ${opts})
# Copy most compile options from the public target, but drop
# -fno-semantic-interposition because _internal libraries are built
# with default (interposable) visibility, which violates the safety
# assumptions for that flag and would skew internal/public benchmarks.
set(safe_opts "${opts}")
list(FILTER safe_opts EXCLUDE REGEX "^-fno-semantic-interposition$")
if(safe_opts)
target_compile_options(${internal} PRIVATE ${safe_opts})
endif()

Copilot uses AI. Check for mistakes.
endif()
endfunction()
2 changes: 2 additions & 0 deletions phlex/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ cet_make_library(
Boost::json
phlex::core
)
phlex_apply_symbol_visibility(phlex_configuration_internal)
phlex_apply_optimizations(phlex_configuration_internal)

cet_make_library(
LIBRARY_NAME
Expand Down
10 changes: 10 additions & 0 deletions phlex/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,20 @@ cet_make_library(
)

install(FILES load_module.hpp run.hpp version.hpp DESTINATION include/phlex/app)
phlex_apply_symbol_visibility(run_phlex)
phlex_apply_optimizations(run_phlex)

# We'll use C++17's filesystem instead of Boost's
target_compile_definitions(run_phlex PRIVATE BOOST_DLL_USE_STD_FS)

phlex_make_internal_library(
run_phlex
LIBRARIES
PUBLIC phlex_core_internal Boost::json Boost::boost
)
# BOOST_DLL_USE_STD_FS is propagated from run_phlex's COMPILE_DEFINITIONS by
# phlex_make_internal_library, so no explicit repeat is needed here.

cet_make_exec(
NAME phlex
SOURCE phlex.cpp
Expand Down
16 changes: 12 additions & 4 deletions phlex/app/load_module.hpp
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
#ifndef PHLEX_APP_LOAD_MODULE_HPP
#define PHLEX_APP_LOAD_MODULE_HPP

#include "phlex/run_phlex_export.hpp"

#include "phlex/core/fwd.hpp"
#include "phlex/driver.hpp"

#include "boost/json.hpp"

#include <functional>
#include <string>

namespace phlex::experimental {
namespace detail {
// Adjust_config adds the module_label as a parameter, and it checks if the 'py'
// parameter exists, inserting the 'cpp: "pymodule"' configuration if necessary.
boost::json::object adjust_config(std::string const& label, boost::json::object raw_config);
run_phlex_EXPORT boost::json::object adjust_config(std::string const& label,
boost::json::object raw_config);
}

void load_module(framework_graph& g, std::string const& label, boost::json::object config);
void load_source(framework_graph& g, std::string const& label, boost::json::object config);
detail::next_index_t load_driver(boost::json::object const& config);
run_phlex_EXPORT void load_module(framework_graph& g,
std::string const& label,
boost::json::object config);
run_phlex_EXPORT void load_source(framework_graph& g,
std::string const& label,
boost::json::object config);
run_phlex_EXPORT detail::next_index_t load_driver(boost::json::object const& config);
}

#endif // PHLEX_APP_LOAD_MODULE_HPP
4 changes: 3 additions & 1 deletion phlex/app/run.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#ifndef PHLEX_APP_RUN_HPP
#define PHLEX_APP_RUN_HPP

#include "phlex/run_phlex_export.hpp"

#include "boost/json.hpp"

#include <optional>

namespace phlex::experimental {
void run(boost::json::object const& configurations, int max_parallelism);
run_phlex_EXPORT void run(boost::json::object const& configurations, int max_parallelism);
}

#endif // PHLEX_APP_RUN_HPP
4 changes: 3 additions & 1 deletion phlex/app/version.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#ifndef PHLEX_APP_VERSION_HPP
#define PHLEX_APP_VERSION_HPP

#include "phlex/run_phlex_export.hpp"

namespace phlex::experimental {
char const* version();
run_phlex_EXPORT char const* version();
}
#endif // PHLEX_APP_VERSION_HPP
4 changes: 3 additions & 1 deletion phlex/concurrency.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#ifndef PHLEX_CONCURRENCY_HPP
#define PHLEX_CONCURRENCY_HPP

#include "phlex/phlex_core_export.hpp"

#include <cstddef>

namespace phlex {
struct concurrency {
struct phlex_core_EXPORT concurrency {
static concurrency const unlimited;
static concurrency const serial;
Comment on lines 10 to 11
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking concurrency as exported does not automatically ensure the out-of-class definitions of the static data members (unlimited, serial) are exported when -fvisibility=hidden is enabled. This can cause link failures for consumers referencing concurrency::unlimited/serial in Release/RelWithDebInfo builds. Export the static data members themselves (declaration and definition), or make them inline variables in the header so no exported definition is required.

Suggested change
static concurrency const unlimited;
static concurrency const serial;
static phlex_core_EXPORT concurrency const unlimited;
static phlex_core_EXPORT concurrency const serial;

Copilot uses AI. Check for mistakes.

Expand Down
Loading
Loading