What is the best way to JIT compile C++ code contained in a string? (original) (raw)

February 28, 2024, 10:08pm 1

Background

I have a code and framework that I’m working on which generates C++ source code for GPU kernels and I need a CPU fallback for systems which don’t support CUDA or HIP, or for when I need to use double or complex types on Metal GPUs. Currently what I’m doing is writing the code to disk foreach thread, fork a process and call the system compiler to generate a dynamic library which I load using dlopen/dlclose. Here an example code with some things omitted for brevity.

void compile(const std::string kernel_source) {
// Create a name for the source code.
    std::ostringstream temp_stream;
    temp_stream << reinterpret_cast<size_t> (this);
    const std::string thread_id = temp_stream.str();

    temp_stream.str(std::string());
    temp_stream.clear();

    temp_stream << "temp_" << thread_id << ".cpp";

    const std::string filename = temp_stream.str();

// Write source code to disk.
    std::ofstream out(filename);
    out << kernel_source;
    out.close();

    temp_stream.str(std::string());
    temp_stream.clear();

// Create a name for the shared library.
    temp_stream << "./" << filename << ".so";
    library_name = temp_stream.str();

    temp_stream.str(std::string());
    temp_stream.clear();

// Generate command line
    temp_stream << CXX << " " << CXX_FLAGS;
    temp_stream << filename << " -o " << library_name;

// Compile the library.
    auto pid = fork();
    int error = 0;
    if (pid == 0) {
        auto args = split_string(temp_stream.str());
        auto c_args = to_c_str(args);
        error = execvp(c_args[0], c_args.data());
    }
    waited(pid, &error, 0);

// Load the library
    lib_handle = dlopen(library_name.c_str(), RTLD_LAZY);
}

From here I load the kernel and return lambda to call it. Note that here the resulting kernels can have different numbers of arguments. I’m using a std::map so I can have consistent function signature which I then unpack inside the kernel.

std::function<void(void)> create_kernel_call(const std::string kernel_name,
                                                                       graph:arg_nodes<T> args) {
// Load kernel function pointer.
    void *kernel = dlsym(lib_handle, kernel_name.c_str());

// Create argument map.
   std::map<size_t, T *> buffers;

    for (auto &arg : args) {
        buffers[reinterpret_cast<size_t> (arg)] = kernel_arguments[arg].data();
    }

// Create a callable function.
    return [kernel, buffers] () mutable {
        ((void (*)(std::map<size_t, T*> &))kernel)(buffers);
    }
}

Where a kernel_source string looks something like

#include <map>

using namespace std;

extern "c" void example_kernel(std::map<size_t, float *> &args) {
    const float v_105553159292360 = args[105553159292360];
    const float o_105553150985176 = args[105553150985176];
    const float o_105553150985816 = args[105553150985816];
    for (size_t i = 0; i < 100000; i++)
        const float r_105553159292360 = v_105553159292360[i];
        const float r_105553150985816 = 10;
        const float r_105553150985416 = -0.300000012;
        const float r_105553150985336 = r_105553159292360 + r_105553150985416;
        const float r_105553150985176 = r_105553150985816*r_105553150985336;
        o_105553150985176[i] = r_105553150985176;
        o_105553150985816[i] = r_105553150985816;
    }
}

This works well enough most of the type but on some systems, dlclose fails to unload. Then a different library can be generated with the same name and dlopen fails to load it.

Using LLVM for JIT

I want to eliminate the need to explicitly write the source to disk and avoid loading the library using the dlopen\dlclose functions. I would also like a better way to handle the fact that the number of kernel arguments can vary in a better way without using std::map.

However, I’m a bit overwhelmed by the JIT user guide and unsure if should focus on MCJIT or ORC. I would be calling this from multiple threads but in a manner where each thread is compiling independent kernels. Some of the kernels will use functions from <cmath> and <complex>. I would need these to apply optimizations as if I was compiling with -O3.

It looks like the LLJIT would replace most of what I’m doing.

auto JIT = LLJITBuilder.create();
JIT->addIRModule(Some thing that converts a string of c++ code to IR module);
auto EntrySym = JIT->lookup("kernel_name");
auto *kernel = EntrySym.getAddress().toPtr<void(*)()> ();

How do I obtain IR for C++ code. I assume this would involve clang libraries? Does this automatically apply the same -O3 optimizations or would I need to apply those when generating the IR? Does this manually handle the run time libraries? Or would I need to use something lower level?

Are there any up-to-date examples of JIT compiling C++ code?

After tripping over llvm::ErrorOr and llvm::Expected a few times, I was able to get a working hello world level example working. I’m still forking to system compiler to generate but I was able to load a function and run it.

#include <string>
#include <fstream>
#include <array>
#include <iostream>

#include <unistd.h>
#include <sys/wait.h>

#include "llvm/Support/TargetSelect.h"
#include "llvm/ExecutionEngine/Orc/LLJIT.h"

int main(int argc, const char * argv[]) {
    llvm::InitializeNativeTarget();
    
    const std::string source(
        "#include <iostream>\n"
        "extern \"C\" void hello() {\n"
        "    std::cout << \"Hello World\" << std::endl;\n"
        "}"
    );

    std::ofstream out("hello.cpp");
    out << source;
    out.close();

    auto pid = fork();
    int error = 0;
    if (pid == 0) {
        auto compiler = std::string("clang");
        auto flag = std::string("-c");
        auto file = std::string("hello.cpp");
        std::array<char *, 4> args = {
            const_cast<char *> (compiler.c_str()),
            const_cast<char *> (flag.c_str()),
            const_cast<char *> (file.c_str()),
            static_cast<char *> (NULL)
        };
        error = execvp(args[0], args.data());
    }
    waitpid(pid, &error, 0);

    auto object = llvm::MemoryBuffer::getFileAsStream("hello.o");

    auto jit_try = llvm::orc::LLJITBuilder().create();
    if (auto jiterror = jit_try.takeError()) {
        std::cerr << "Failed to build JIT : " << toString(std::move(jiterror)) << std::endl;
        return 1;
    }
    auto jit = std::move(jit_try.get());

    jit->addObjectFile(std::move(object.get()));
    llvm::orc::ExecutorAddr entry = std::move(jit->lookup("hello")).get();
    auto kernel = entry.toPtr<void(*)()> ();

    kernel();

    return 0;
}

Anyee March 7, 2024, 1:04am 3

I think you should lookup how to invoke Clang compiler from in-process way and trap errors; that way the Clang API can give you a LLVM Module and you can generate bitcode and perhaps run this on your CPU or accelerator etc.

Using the example I found here, I am able to get a to a point where I can get clang to emit some error messages.

#include <string>
#include <iostream>
#include <memory>

#include "clang/Frontend/TextDiagnosticPrinter.h"
#include "clang/Frontend/CompilerInvocation.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/CodeGen/CodeGenAction.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/ExecutionEngine/Orc/LLJIT.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/TargetParser/Host.h"

void jitFunction(const std::string message) {
    llvm::InitializeNativeTarget();
    
    const std::string source(
        "#include <iostream>\n"
        "extern \"C\" void hello() {\n"
        "    std::cout << \"" + message + "\" << std::endl;\n"
        "}"
    );

    clang::DiagnosticOptions diagOpts;
    auto textDiagPrinter = std::make_unique<clang::TextDiagnosticPrinter> (llvm::errs(),
                                                                           &diagOpts);

    llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> pDiagIDs;
    clang::DiagnosticsEngine DE(pDiagIDs, &diagOpts,
                                textDiagPrinter.get());

    std::vector<const char *> args = {
        "-O3",
        "hello.cpp"
    };

    auto CI = std::make_shared<clang::CompilerInvocation> ();
    clang::CompilerInvocation::CreateFromArgs(*(CI.get()), args, DE);

    llvm::StringRef source_code_data(source);
    auto buffer = llvm::MemoryBuffer::getMemBufferCopy(source_code_data);
    CI->getPreprocessorOpts().addRemappedFile(args[1], buffer.get());

    clang::CompilerInstance clang;
    clang.setInvocation(CI);
    clang.createDiagnostics();

    const auto targetOptions = std::make_shared<clang::TargetOptions> ();
    targetOptions->Triple = llvm::sys::getProcessTriple();
    auto *pTI = clang::TargetInfo::CreateTargetInfo(DE, targetOptions);
    clang.setTarget(pTI);

    clang::EmitObjAction compilerAction;
    clang.ExecuteAction(compilerAction);

    auto object = llvm::MemoryBuffer::getFileAsStream("hello.o");

    auto jit_try = llvm::orc::LLJITBuilder().create();
    if (auto jiterror = jit_try.takeError()) {
        std::cerr << "Failed to build JIT : " << toString(std::move(jiterror)) << std::endl;
        exit(1);
    }
    auto jit = std::move(jit_try.get());

    jit->addObjectFile(std::move(object.get()));
    auto entry = std::move(jit->lookup("hello")).get();
    auto kernel = entry.toPtr<void(*)()> ();

    kernel();
}

But the problem I’m running into now is trying to find the c++ standard library headers. Adding explicit include paths to args

    std::vector<const char *> args = {
        "-O3",
        "hello.cpp",
        "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/",
        "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/include",
        "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include"
    };

results in the error.

In file included from hello.cpp:1:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream:43:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/ios:221:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__ios/fpos.h:14:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iosfwd:106:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__std_mbstate_t.h:14:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__mbstate_t.h:45:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mbstate_t.h:31:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/types.h:37:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/types.h:50:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h:81:2: warning: "Unsupported compiler detected" [-W#warnings]
   81 | #warning "Unsupported compiler detected"
      |  ^
In file included from hello.cpp:1:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream:43:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/ios:222:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__locale:21:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/mutex:192:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__condition_variable/condition_variable.h:17:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__mutex/unique_lock.h:17:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__system_error/system_error.h:14:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__system_error/error_category.h:15:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/string:569:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__algorithm/remove.h:12:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__algorithm/find.h:21:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/cwchar:114:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/wchar.h:126:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/wchar.h:90:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/stdio.h:108:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h:64:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h:75:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_va_list.h:32:26: error: typedef redefinition with different types ('__darwin_va_list' (aka 'void *') vs '__builtin_va_list')
   32 | typedef __darwin_va_list va_list;
      |                          ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/include/stdarg.h:14:27: note: previous definition is here
   14 | typedef __builtin_va_list va_list;
      |                           ^
1 warning and 1 error generated.

However, I would like to avoid hard coding these paths since I intend this to be cross platform. What is the best way to find the system headers?

I was able to get the example working.

#include <string>
#include <iostream>
#include <memory>

#include "clang/Frontend/TextDiagnosticPrinter.h"
#include "clang/Frontend/CompilerInvocation.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/CodeGen/CodeGenAction.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/ExecutionEngine/Orc/LLJIT.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/TargetParser/Host.h"
#include "llvm/ExecutionEngine/Orc/ThreadSafeModule.h"

void jitFunction(const std::string message) {
    llvm::InitializeNativeTarget();
    LLVMInitializeAArch64AsmPrinter();
    LLVMInitializeAArch64AsmParser();
    
    const std::string source(
        "#include <iostream>\n"
        "extern \"C\" void hello() {\n"
        "    std::cout << \"" + message + "\" << std::endl;\n"
        "}"
    );

    llvm::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagOpts;
    auto textDiagPrinter = std::make_unique<clang::TextDiagnosticPrinter> (llvm::errs(),
                                                                           diagOpts.get());

    llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> pDiagIDs;
    clang::DiagnosticsEngine DE(pDiagIDs, diagOpts,
                                textDiagPrinter.release());

    std::vector<const char *> args = {
        "-O3",
        "hello.cpp",
        "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1",
        "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/include",
        "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include",
        "-fgnuc-version=4.2.1"
    };

    auto CI = std::make_shared<clang::CompilerInvocation> ();
    clang::CompilerInvocation::CreateFromArgs(*(CI.get()), args, DE);

    llvm::StringRef source_code_data(source);
    auto buffer = llvm::MemoryBuffer::getMemBuffer(source_code_data);
    CI->getPreprocessorOpts().addRemappedFile(args[1], buffer.release());

    clang::CompilerInstance clang;
    clang.setInvocation(CI);
    clang.createDiagnostics();

    const auto targetOptions = std::make_shared<clang::TargetOptions> ();
    targetOptions->Triple = llvm::sys::getProcessTriple();
    auto *pTI = clang::TargetInfo::CreateTargetInfo(DE, targetOptions);
    clang.setTarget(pTI);

    clang::EmitCodeGenOnlyAction compilerAction;
    clang.ExecuteAction(compilerAction);

    auto ir_module = compilerAction.takeModule();
    auto ctx = std::unique_ptr<llvm::LLVMContext> (compilerAction.takeLLVMContext());

    auto jit_try = llvm::orc::LLJITBuilder().create();
    if (auto jiterror = jit_try.takeError()) {
        std::cerr << "Failed to build JIT : " << toString(std::move(jiterror)) << std::endl;
        exit(1);
    }
    auto jit = std::move(jit_try.get());

    jit->addIRModule(llvm::orc::ThreadSafeModule(std::move(ir_module),
                                                 llvm::orc::ThreadSafeContext(std::move(ctx))));

    auto entry = std::move(jit->lookup("hello")).get();
    auto kernel = entry.toPtr<void(*)()> ();

    kernel();
}

int main(int argc, const char * argv[]) {
    jitFunction("Hello World1");
    jitFunction("Hello World2");
    jitFunction("Hello World3");

    return 0;
}

However, I would like to avoid hard coding these paths since I intend this to be cross platform. What is the best way to find the system headers?

I tested with system clang with clang -### demo.cc, and I got

➜  ~ clang -### demo.cc
Apple clang version 14.0.3 (clang-1403.0.22.14.1)
Target: arm64-apple-darwin22.5.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
 "/Library/Developer/CommandLineTools/usr/bin/clang" "-cc1" "-triple" "arm64-apple-macosx13.0.0" "-Wundef-prefix=TARGET_OS_" "-Wdeprecated-objc-isa-usage" "-Werror=deprecated-objc-isa-usage" "-Werror=implicit-function-declaration" "-emit-obj" "-mrelax-all" "--mrelax-relocations" "-disable-free" "-clear-ast-before-backend" "-disable-llvm-verifier" "-discard-value-names" "-main-file-name" "demo.cc" "-mrelocation-model" "pic" "-pic-level" "2" "-mframe-pointer=non-leaf" "-fno-strict-return" "-ffp-contract=on" "-fno-rounding-math" "-funwind-tables=1" "-fobjc-msgsend-selector-stubs" "-target-sdk-version=13.3" "-fvisibility-inlines-hidden-static-local-var" "-target-cpu" "apple-m1" "-target-feature" "+v8.5a" "-target-feature" "+crc" "-target-feature" "+lse" "-target-feature" "+rdm" "-target-feature" "+crypto" "-target-feature" "+dotprod" "-target-feature" "+fp-armv8" "-target-feature" "+neon" "-target-feature" "+fp16fml" "-target-feature" "+ras" "-target-feature" "+rcpc" "-target-feature" "+zcm" "-target-feature" "+zcz" "-target-feature" "+fullfp16" "-target-feature" "+sm4" "-target-feature" "+sha3" "-target-feature" "+sha2" "-target-feature" "+aes" "-target-abi" "darwinpcs" "-fallow-half-arguments-and-returns" "-mllvm" "-treat-scalable-fixed-error-as-warning" "-debugger-tuning=lldb" "-target-linker-version" "857.1" "-fcoverage-compilation-dir=/Users/shuoshu.yh" "-resource-dir" "/Library/Developer/CommandLineTools/usr/lib/clang/14.0.3" "-isysroot" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" "-I/usr/local/include" "-stdlib=libc++" "-internal-isystem" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1" "-internal-isystem" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/local/include" "-internal-isystem" "/Library/Developer/CommandLineTools/usr/lib/clang/14.0.3/include" "-internal-externc-isystem" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" "-internal-externc-isystem" "/Library/Developer/CommandLineTools/usr/include" "-Wno-reorder-init-list" "-Wno-implicit-int-float-conversion" "-Wno-c99-designator" "-Wno-final-dtor-non-final-class" "-Wno-extra-semi-stmt" "-Wno-misleading-indentation" "-Wno-quoted-include-in-framework-header" "-Wno-implicit-fallthrough" "-Wno-enum-enum-conversion" "-Wno-enum-float-conversion" "-Wno-elaborated-enum-base" "-Wno-reserved-identifier" "-Wno-gnu-folding-constant" "-fdeprecated-macro" "-fdebug-compilation-dir=/Users/shuoshu.yh" "-ferror-limit" "19" "-stack-protector" "1" "-fstack-check" "-mdarwin-stkchk-strong-link" "-fblocks" "-fencode-extended-block-signature" "-fregister-global-dtors-with-atexit" "-fgnuc-version=4.2.1" "-fno-cxx-modules" "-no-opaque-pointers" "-fcxx-exceptions" "-fexceptions" "-fmax-type-align=16" "-fcommon" "-fcolor-diagnostics" "-clang-vendor-feature=+disableNonDependentMemberExprInCurrentInstantiation" "-fno-odr-hash-protocols" "-clang-vendor-feature=+enableAggressiveVLAFolding" "-clang-vendor-feature=+revert09abecef7bbf" "-clang-vendor-feature=+thisNoAlignAttr" "-clang-vendor-feature=+thisNoNullAttr" "-mllvm" "-disable-aligned-alloc-awareness=1" "-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o" "/var/folders/8j/094tp2hd3h986hp51n_3pgz80000gp/T/demo-627ba4.o" "-x" "c++" "demo.cc"
 "/Library/Developer/CommandLineTools/usr/bin/ld" "-demangle" "-lto_library" "/Library/Developer/CommandLineTools/usr/lib/libLTO.dylib" "-no_deduplicate" "-dynamic" "-arch" "arm64" "-platform_version" "macos" "13.0.0" "13.3" "-syslibroot" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" "-o" "a.out" "-L/usr/local/lib" "/var/folders/8j/094tp2hd3h986hp51n_3pgz80000gp/T/demo-627ba4.o" "-lSystem" "/Library/Developer/CommandLineTools/usr/lib/clang/14.0.3/lib/darwin/libclang_rt.osx.a"

the system related options is

-resource-dir" "/Library/Developer/CommandLineTools/usr/lib/clang/14.0.3" "-isysroot" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" "-I/usr/local/include" "-stdlib=libc++" "-internal-isystem" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1" "-internal-isystem" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/local/include" "-internal-isystem" "/Library/Developer/CommandLineTools/usr/lib/clang/14.0.3/include" "-internal-externc-isystem" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" "-internal-externc-isystem" "/Library/Developer/CommandLineTools/usr/include"

I found use xcrun --sdk macosx --show-sdk-path, then we got

➜  ~ xcrun --sdk macosx --show-sdk-path
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk

I also found

➜  ~ clang -print-resource-dir
/Library/Developer/CommandLineTools/usr/lib/clang/14.0.3

for "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1", I have no idea.