diff --git a/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-abnormal-exit-code.s b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-abnormal-exit-code.s new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-abnormal-exit-code.s @@ -0,0 +1,9 @@ +# REQUIRES: exegesis-can-execute-in-subprocess, x86_64-linux + +# RUN: llvm-exegesis -mtriple=x86_64-unknown-unknown -mode=latency -snippets-file=%s -execution-mode=subprocess | FileCheck %s + +# CHECK: error: 'Child benchmarking process exited with non-zero exit code: Child process returned with unknown exit code' + +movl $60, %eax +movl $127, %edi +syscall diff --git a/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-segfault.s b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-segfault.s new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-segfault.s @@ -0,0 +1,8 @@ +# REQUIRES: exegesis-can-execute-in-subprocess, x86_64-linux + +# RUN: llvm-exegesis -mtriple=x86_64-unknown-unknown -mode=latency -snippets-file=%s -execution-mode=subprocess | FileCheck %s + +# CHECK: error: 'The benchmarking subprocess sent unexpected signal: Segmentation fault' + +# LLVM-EXEGESIS-DEFREG RBX 0 +movq (%rbx), %rax diff --git a/llvm/test/tools/llvm-exegesis/X86/latency/subprocess.s b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess.s new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess.s @@ -0,0 +1,11 @@ +# REQUIRES: exegesis-can-execute-in-subprocess, x86_64-linux + +# RUN: llvm-exegesis -mtriple=x86_64-unknown-unknown -mode=latency -snippets-file=%s -execution-mode=subprocess | FileCheck %s + +# CHECK: measurements: +# CHECK-NEXT: value: {{.*}}, per_snippet_value: {{.*}} + +# LLVM-EXEGESIS-DEFREG XMM1 42 +# LLVM-EXEGESIS-DEFREG XMM2 42 +# LLVM-EXEGESIS-DEFREG XMM3 42 +vhaddps %xmm2, %xmm2, %xmm3 diff --git a/llvm/test/tools/llvm-exegesis/lit.local.cfg b/llvm/test/tools/llvm-exegesis/lit.local.cfg --- a/llvm/test/tools/llvm-exegesis/lit.local.cfg +++ b/llvm/test/tools/llvm-exegesis/lit.local.cfg @@ -1,4 +1,5 @@ import subprocess +import re import lit.util @@ -30,6 +31,35 @@ return False +def can_execute_in_subprocess(): + # We need certain Linux system calls present in order to run the subprocess + # executor mode, so check that we can use the subprocess mode to prevent + # test failures on platforms running older kernels. + llvm_exegesis_exe = lit.util.which("llvm-exegesis", config.llvm_tools_dir) + if llvm_exegesis_exe is None: + print("could not find llvm-exegesis") + return False + try: + command_vector = [ + llvm_exegesis_exe, + "-mode=latency", + "--execution-mode=subprocess", + "-snippets-file=/dev/null", + ] + with subprocess.Popen( + command_vector, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) as exegesis_subprocess: + stdout, stderr = exegesis_subprocess.communicate() + exegesis_output = stdout.decode("utf-8") + # Return true if we have an empty error section as llvm-exegesis + # doesn't change the return code if there's a snippet crash. + return re.search("error:\s*''", exegesis_output) is not None + + except OSError: + print("Could not execute llvm-exegesis in subprocess executor mode") + return False + + for arch in ["aarch64", "mips", "powerpc", "x86_64"]: if can_execute_generated_snippets(arch): config.available_features.add("exegesis-can-execute-%s" % arch) @@ -46,3 +76,7 @@ "latency", ["-x86-lbr-sample-period", "123", "-repetition-mode", "loop"] ): config.available_features.add("exegesis-can-measure-latency-lbr") + +if can_execute_in_subprocess(): + print("Passed thing") + config.available_features.add("exegesis-can-execute-in-subprocess") diff --git a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h --- a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h +++ b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h @@ -34,7 +34,7 @@ // Common code for all benchmark modes. class BenchmarkRunner { public: - enum ExecutionModeE { InProcess }; + enum ExecutionModeE { InProcess, SubProcess }; explicit BenchmarkRunner(const LLVMState &State, Benchmark::ModeE Mode, BenchmarkPhaseSelectorE BenchmarkPhaseSelector, diff --git a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp --- a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp +++ b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp @@ -25,6 +25,18 @@ #include "llvm/Support/FileSystem.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Program.h" +#include "llvm/Support/Signals.h" + +#ifdef __linux__ +#ifdef HAVE_LIBPFM +#include +#endif +#include +#include +#include +#include +#include +#endif // __linux__ namespace llvm { namespace exegesis { @@ -129,6 +141,182 @@ const ExecutableFunction Function; BenchmarkRunner::ScratchSpace *const Scratch; }; + +#ifdef __linux__ +// The following class implements a function executor that executes the +// benchmark code within a subprocess rather than within the main llvm-exegesis +// process. This allows for much more control over the execution context of the +// snippet, particularly with regard to memory. This class performs all the +// necessary functions to create the subprocess, execute the snippet in the +// subprocess, and report results/handle errors. +class SubProcessFunctionExecutorImpl + : public BenchmarkRunner::FunctionExecutor { +public: + SubProcessFunctionExecutorImpl(const LLVMState &State, + object::OwningBinary Obj, + const BenchmarkKey &Key) + : State(State), Function(State.createTargetMachine(), std::move(Obj)), + Key(Key) {} + +private: + enum ChildProcessExitCodeE { + CounterFDReadFailed = 1, + TranslatingCounterFDFailed + }; + + StringRef childProcessExitCodeToString(int ExitCode) const { + switch (ExitCode) { + case ChildProcessExitCodeE::CounterFDReadFailed: + return "Counter file descriptor read failed"; + case ChildProcessExitCodeE::TranslatingCounterFDFailed: + return "Translating counter file descriptor into a file descriptor in " + "the child process failed. This might be due running an older " + "Linux kernel that doesn't support the pidfd_getfd system call " + "(anything before Linux 5.6)."; + default: + return "Child process returned with unknown exit code"; + } + } + + Error createSubProcessAndRunBenchmark( + StringRef CounterName, SmallVectorImpl &CounterValues) const { + int PipeFiles[2]; + int PipeSuccessOrErr = pipe(PipeFiles); + if (PipeSuccessOrErr != 0) { + return make_error( + "Failed to create a pipe for interprocess communication between " + "llvm-exegesis and the benchmarking subprocess"); + } + + pid_t ParentOrChildPID = fork(); + if (ParentOrChildPID == 0) { + // We are in the child process, close the write end of the pipe + close(PipeFiles[1]); + // Unregister handlers, signal handling is now handled through ptrace in + // the host process + llvm::sys::unregisterHandlers(); + prepareAndRunBenchmark(PipeFiles[0], Key); + // The child process terminates in the above function, so we should never + // get to this point. + llvm_unreachable("Child process didn't exit when expected."); + } + + const ExegesisTarget &ET = State.getExegesisTarget(); + auto CounterOrError = + ET.createCounter(CounterName, State, ParentOrChildPID); + + if (!CounterOrError) + return CounterOrError.takeError(); + + pfm::Counter *Counter = CounterOrError.get().get(); + + close(PipeFiles[0]); + + int CounterFileDescriptor = Counter->getFileDescriptor(); + ssize_t BytesWritten = + write(PipeFiles[1], &CounterFileDescriptor, sizeof(int)); + + if (BytesWritten != sizeof(int)) + return make_error("Writing peformance counter file descriptor " + "to child process failed: " + + Twine(strerror(errno))); + + if (ptrace(PTRACE_SEIZE, ParentOrChildPID, NULL, NULL) != 0) + return make_error("Failed to seize the child process: " + + Twine(strerror(errno))); + + int ChildStatus; + if (wait(&ChildStatus) == -1) { + return make_error( + "Waiting for the child process to complete failed: " + + Twine(strerror(errno))); + } + + if (WIFEXITED(ChildStatus)) { + int ChildExitCode = WEXITSTATUS(ChildStatus); + if (ChildExitCode == 0) { + // The child exited succesfully, read counter values and return + // success + CounterValues[0] = Counter->read(); + return Error::success(); + } + // The child exited, but not successfully + return make_error( + "Child benchmarking process exited with non-zero exit code: " + + childProcessExitCodeToString(ChildExitCode)); + } + + // An error was encountered running the snippet, process it + siginfo_t ChildSignalInfo; + if (ptrace(PTRACE_GETSIGINFO, ParentOrChildPID, NULL, &ChildSignalInfo) == + -1) { + return make_error("Getting signal info from the child failed: " + + Twine(strerror(errno))); + } + + return make_error( + "The benchmarking subprocess sent unexpected signal: " + + Twine(strsignal(ChildSignalInfo.si_signo))); + } + + [[noreturn]] void prepareAndRunBenchmark(int Pipe, + const BenchmarkKey &Key) const { + // The following occurs within the benchmarking subprocess + + int ParentCounterFileDescriptor = -1; + ssize_t BytesRead = read(Pipe, &ParentCounterFileDescriptor, sizeof(int)); + + if (BytesRead != sizeof(int)) { + exit(ChildProcessExitCodeE::CounterFDReadFailed); + } + + // Make sure the following two syscalls are defined on the platform that + // we're building on as they were introduced to the kernel fairly recently + // (v5.6 for the second one). +#if defined SYS_pidfd_open && defined SYS_pidfd_getfd + pid_t ParentPID = getppid(); + + int ParentPIDFD = syscall(SYS_pidfd_open, ParentPID, 0); + int CounterFileDescriptor = + syscall(SYS_pidfd_getfd, ParentPIDFD, ParentCounterFileDescriptor, 0); +#else + int CounterFileDescriptor = 0; + exit(ChildProcessExitCodeE::TranslatingCounterFDFailed); +#endif + + if (CounterFileDescriptor == -1) { + exit(ChildProcessExitCodeE::TranslatingCounterFDFailed); + } + +#ifdef HAVE_LIBPFM + ioctl(CounterFileDescriptor, PERF_EVENT_IOC_RESET); +#endif + this->Function(nullptr); +#ifdef HAVE_LIBPFM + ioctl(CounterFileDescriptor, PERF_EVENT_IOC_DISABLE); +#endif + + exit(0); + } + + Expected> + runWithCounter(StringRef CounterName) const override { + SmallVector Value(1, 0); + Error PossibleBenchmarkError = + createSubProcessAndRunBenchmark(CounterName, Value); + + if (PossibleBenchmarkError) { + return std::move(PossibleBenchmarkError); + } + + return Value; + } + + const LLVMState &State; + const ExecutableFunction Function; + const BenchmarkKey &Key; +}; +#endif // __linux__ } // namespace Expected> BenchmarkRunner::assembleSnippet( @@ -201,6 +389,14 @@ case ExecutionModeE::InProcess: return std::make_unique( State, std::move(ObjectFile), Scratch.get()); + case ExecutionModeE::SubProcess: +#ifdef __linux__ + return std::make_unique( + State, std::move(ObjectFile), Key); +#else + return make_error( + "The subprocess execution mode is only supported on Linux"); +#endif } llvm_unreachable("ExecutionMode is outside expected range"); } diff --git a/llvm/tools/llvm-exegesis/llvm-exegesis.cpp b/llvm/tools/llvm-exegesis/llvm-exegesis.cpp --- a/llvm/tools/llvm-exegesis/llvm-exegesis.cpp +++ b/llvm/tools/llvm-exegesis/llvm-exegesis.cpp @@ -254,7 +254,11 @@ cl::cat(BenchmarkOptions), cl::values(clEnumValN(BenchmarkRunner::ExecutionModeE::InProcess, "inprocess", - "Executes the snippets within the same process")), + "Executes the snippets within the same process"), + clEnumValN(BenchmarkRunner::ExecutionModeE::SubProcess, + "subprocess", + "Spawns a subprocess for each snippet execution, " + "allows for the use of memory annotations")), cl::init(BenchmarkRunner::ExecutionModeE::InProcess)); static ExitOnError ExitOnErr("llvm-exegesis error: ");