From 4d618b52f6e05e41d35f56653cb36bf7d4dc794e Mon Sep 17 00:00:00 2001 From: Aiden Grossman Date: Sat, 20 May 2023 09:23:27 +0000 Subject: [PATCH] [llvm-exegesis] Introduce Subprocess Executor Mode This patch introduces the subprocess executor mode. Currently, this new mode doesn't do anything fancy, just executing the same code that the inprocess executor would do, but within a subprocess. This sets up the ability to add in many more memory-related features in the future. --- .../X86/latency/subprocess-abnormal-exit-code.s | 9 + .../X86/latency/subprocess-segfault.s | 8 + .../tools/llvm-exegesis/X86/latency/subprocess.s | 11 ++ llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp | 196 +++++++++++++++++++++ llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h | 2 +- llvm/tools/llvm-exegesis/llvm-exegesis.cpp | 6 +- 6 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 llvm/test/tools/llvm-exegesis/X86/latency/subprocess-abnormal-exit-code.s create mode 100644 llvm/test/tools/llvm-exegesis/X86/latency/subprocess-segfault.s create mode 100644 llvm/test/tools/llvm-exegesis/X86/latency/subprocess.s 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 index 0000000..9e28982 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-abnormal-exit-code.s @@ -0,0 +1,9 @@ +# REQUIRES: exegesis-can-execute-x86_64, exegesis-can-measure-latency, 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 index 0000000..56a5c18 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess-segfault.s @@ -0,0 +1,8 @@ +# REQUIRES: exegesis-can-execute-x86_64, exegesis-can-measure-latency, 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 index 0000000..a8cfe9f --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/subprocess.s @@ -0,0 +1,11 @@ +# REQUIRES: exegesis-can-execute-x86_64, exegesis-can-measure-latency, 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/tools/llvm-exegesis/lib/BenchmarkRunner.cpp b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp index 09539e8..b0dc0b7 100644 --- 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 @@ private: 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 @@ BenchmarkRunner::createFunctionExecutor( 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/lib/BenchmarkRunner.h b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h index e5185b4..88559901 100644 --- a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h +++ b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h @@ -34,7 +34,7 @@ namespace exegesis { // 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/llvm-exegesis.cpp b/llvm/tools/llvm-exegesis/llvm-exegesis.cpp index d890fbb..82df07c 100644 --- a/llvm/tools/llvm-exegesis/llvm-exegesis.cpp +++ b/llvm/tools/llvm-exegesis/llvm-exegesis.cpp @@ -254,7 +254,11 @@ static cl::opt ExecutionMode( 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: "); -- 2.7.4