Slow generation on MacOS due to repeated calls to cmSystemTools::GuessLibraryInstallName()
I have a project with a large number of targets (in the order of thousands) and have noticed that generation is considerably slower on MacOS than it is on Linux, sometimes taking twice as long.
After profiling CMake while configuring my project, I have noticed that significant time is spent in cmSystemTools::GuessLibraryInstallName()
, specifically as called here: https://gitlab.kitware.com/cmake/cmake/-/blob/master/Source/cmGeneratorTarget.cxx#L2036
I have been able to reproduce this with a minimal project, along the lines of:
find_package(ZLIB REQUIRED)
foreach(i RANGE 20000)
add_executable(foo_${i} foo.cpp)
target_link_libraries(foo_${i} PRIVATE ZLIB::ZLIB)
endforeach()
On Linux: 9 seconds
On MacOS: 23 seconds
On systems with similar specs. In both cases I'm using the Ninja generator, and measuring the time it takes to call cmake .
inside the build directory after the cache has already been initialised (so this excludes the time detecting the compiler and so on). On MacOS it finds the system provided zlib (/usr/lib/libz.dylib
), on Linux this would require zlib to be installed otherwise.
What I see makes sense: CMake needs to determine whether the library has an install name that starts with @rpath
and if it does, add the path to that library to the rpath of the target I'm compiling. In profiling, I can see that cmSystemTools::GuessLibraryInstallName()
gets called the 20000 times for exactly the same file: /usr/lib/libz.dylib
.
In a real life project the situation is more varied, but still: I can see about 20k calls to that function for a small subset of files. I believe it's parsing the Mach-O headers to retrieve the install name.
Based on cmGeneratorTarget::HasMacOSXRpathInstallNameDir
it's easy to see which libraries end up calling this function:
- Any library that comes from an imported target defined in a Find module. This includes static libraries. This makes sense as those target are typically set up as
UNKNOWN
. - Imported targets from config files that don't populate the
IMPORTED_SONAME_RELEASE
property. This is the case for config files that are not generated by CMake itself, such as (recent) Boost and Qt. I have observed that CMake's generated config files do include this property if the library was built with an install name that starts with@rpath
), so these are not an issue.
One oddity: I would have expected that setting CMAKE_SKIP_BUILD_RPATH
or CMAKE_SKIP_RPATH
to OFF
would have removed the need to inspect these files, but this wasn't the case.
Based on what I see in the profiler, I believe a similar issue happens here: https://gitlab.kitware.com/cmake/cmake/-/blob/master/Source/cmLocalGenerator.cxx#L1688 where the NameResolvesToFramework()
function is being repeatedly called on the same directories.
Would it be possible to implement a cache so that these queries are only performed once for the same file (or directory) during the same cmake
run? I would be happy to contribute, I just wonder if there are already similar cases in the codebase that I can mirror. After some fiddling with the code, I cobbled together a cached implementation of this function and managed to get those 23 seconds down to about 15.