From 9886788a8a500a1b429a6db64397c849b112251c Mon Sep 17 00:00:00 2001 From: Aiden Grossman Date: Thu, 19 Sep 2024 06:34:21 -0700 Subject: [PATCH] [llvm-exegesis] Add support for pinning benchmarking process to a CPU (#85168) This patch adds in support for pinning a benchmarking process to a specific CPU (in the subprocess benchmarking mode on Linux). This is intended to be used in environments where a certain set of CPUs is isolated from the scheduler using something like cgroups and thus should present less potential for noise than normal. This also opens up the door for doing multithreaded benchmarking as we can now pin benchmarking processes to specific CPUs that we know won't interfere with each other. --- .../X86/latency/cpu-pinning-execution-mode.s | 5 ++ .../llvm-exegesis/X86/latency/cpu-pinning.s | 5 ++ .../llvm-exegesis/lib/BenchmarkRunner.cpp | 64 +++++++++++++++---- .../tools/llvm-exegesis/lib/BenchmarkRunner.h | 6 +- llvm/tools/llvm-exegesis/llvm-exegesis.cpp | 11 +++- 5 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning-execution-mode.s create mode 100644 llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning.s diff --git a/llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning-execution-mode.s b/llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning-execution-mode.s new file mode 100644 index 00000000000000..b73ac26f2cfc74 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning-execution-mode.s @@ -0,0 +1,5 @@ +# REQUIRES: exegesis-can-measure-latency, x86_64-linux + +# RUN: not llvm-exegesis -mtriple=x86_64-unknown-unknown -mode=latency -opcode-name=ADD64rr -execution-mode=inprocess --benchmark-process-cpu=0 2>&1 | FileCheck %s + +# CHECK: llvm-exegesis error: The inprocess execution mode does not support benchmark core pinning. diff --git a/llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning.s b/llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning.s new file mode 100644 index 00000000000000..0ea3752fc3bb95 --- /dev/null +++ b/llvm/test/tools/llvm-exegesis/X86/latency/cpu-pinning.s @@ -0,0 +1,5 @@ +# REQUIRES: exegesis-can-measure-latency, x86_64-linux + +# RUN: llvm-exegesis -mtriple=x86_64-unknown-unknown -mode=latency -opcode-name=ADD64rr -execution-mode=subprocess | FileCheck %s + +# CHECK: - { key: latency, value: {{[0-9.]*}}, per_snippet_value: {{[0-9.]*}} diff --git a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp index 4e60d33fa2c637..27b5b3084cbcc2 100644 --- a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp +++ b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.cpp @@ -98,7 +98,8 @@ class InProcessFunctionExecutorImpl : public BenchmarkRunner::FunctionExecutor { public: static Expected> create(const LLVMState &State, object::OwningBinary Obj, - BenchmarkRunner::ScratchSpace *Scratch) { + BenchmarkRunner::ScratchSpace *Scratch, + std::optional BenchmarkProcessCPU) { Expected EF = ExecutableFunction::create(State.createTargetMachine(), std::move(Obj)); @@ -190,27 +191,31 @@ class SubProcessFunctionExecutorImpl public: static Expected> create(const LLVMState &State, object::OwningBinary Obj, - const BenchmarkKey &Key) { + const BenchmarkKey &Key, std::optional BenchmarkProcessCPU) { Expected EF = ExecutableFunction::create(State.createTargetMachine(), std::move(Obj)); if (!EF) return EF.takeError(); return std::unique_ptr( - new SubProcessFunctionExecutorImpl(State, std::move(*EF), Key)); + new SubProcessFunctionExecutorImpl(State, std::move(*EF), Key, + BenchmarkProcessCPU)); } private: SubProcessFunctionExecutorImpl(const LLVMState &State, ExecutableFunction Function, - const BenchmarkKey &Key) - : State(State), Function(std::move(Function)), Key(Key) {} + const BenchmarkKey &Key, + std::optional BenchmarkCPU) + : State(State), Function(std::move(Function)), Key(Key), + BenchmarkProcessCPU(BenchmarkCPU) {} enum ChildProcessExitCodeE { CounterFDReadFailed = 1, RSeqDisableFailed, FunctionDataMappingFailed, - AuxiliaryMemorySetupFailed + AuxiliaryMemorySetupFailed, + SetCPUAffinityFailed }; StringRef childProcessExitCodeToString(int ExitCode) const { @@ -223,6 +228,8 @@ class SubProcessFunctionExecutorImpl return "Failed to map memory for assembled snippet"; case ChildProcessExitCodeE::AuxiliaryMemorySetupFailed: return "Failed to setup auxiliary memory"; + case ChildProcessExitCodeE::SetCPUAffinityFailed: + return "Failed to set CPU affinity of the benchmarking process"; default: return "Child process returned with unknown exit code"; } @@ -384,6 +391,29 @@ class SubProcessFunctionExecutorImpl return make_error(ChildSignalInfo.si_signo); } + static void setCPUAffinityIfRequested(int CPUToUse) { + // Set the CPU affinity for the child process, so that we ensure that if + // the user specified a CPU the process should run on, the benchmarking + // process is running on that CPU. + cpu_set_t CPUMask; + CPU_ZERO(&CPUMask); + CPU_SET(CPUToUse, &CPUMask); + // TODO(boomanaiden154): Rewrite this to use LLVM primitives once they + // are available. + int SetAffinityReturn = sched_setaffinity(0, sizeof(CPUMask), &CPUMask); + if (SetAffinityReturn == -1) { + exit(ChildProcessExitCodeE::SetCPUAffinityFailed); + } + + // Check (if assertions are enabled) that we are actually running on the + // CPU that was specified by the user. + unsigned int CurrentCPU; + assert(getcpu(&CurrentCPU, nullptr) == 0 && + "Expected getcpu call to succeed."); + assert(static_cast(CurrentCPU) == CPUToUse && + "Expected current CPU to equal the CPU requested by the user"); + } + Error createSubProcessAndRunBenchmark( StringRef CounterName, SmallVectorImpl &CounterValues, ArrayRef ValidationCounters, @@ -416,6 +446,10 @@ class SubProcessFunctionExecutorImpl } if (ParentOrChildPID == 0) { + if (BenchmarkProcessCPU.has_value()) { + setCPUAffinityIfRequested(*BenchmarkProcessCPU); + } + // 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 @@ -538,6 +572,7 @@ class SubProcessFunctionExecutorImpl const LLVMState &State; const ExecutableFunction Function; const BenchmarkKey &Key; + const std::optional BenchmarkProcessCPU; }; #endif // __linux__ } // namespace @@ -615,11 +650,15 @@ BenchmarkRunner::getRunnableConfiguration( Expected> BenchmarkRunner::createFunctionExecutor( object::OwningBinary ObjectFile, - const BenchmarkKey &Key) const { + const BenchmarkKey &Key, std::optional BenchmarkProcessCPU) const { switch (ExecutionMode) { case ExecutionModeE::InProcess: { + if (BenchmarkProcessCPU.has_value()) + return make_error("The inprocess execution mode does not " + "support benchmark core pinning."); + auto InProcessExecutorOrErr = InProcessFunctionExecutorImpl::create( - State, std::move(ObjectFile), Scratch.get()); + State, std::move(ObjectFile), Scratch.get(), BenchmarkProcessCPU); if (!InProcessExecutorOrErr) return InProcessExecutorOrErr.takeError(); @@ -628,7 +667,7 @@ BenchmarkRunner::createFunctionExecutor( case ExecutionModeE::SubProcess: { #ifdef __linux__ auto SubProcessExecutorOrErr = SubProcessFunctionExecutorImpl::create( - State, std::move(ObjectFile), Key); + State, std::move(ObjectFile), Key, BenchmarkProcessCPU); if (!SubProcessExecutorOrErr) return SubProcessExecutorOrErr.takeError(); @@ -643,8 +682,8 @@ BenchmarkRunner::createFunctionExecutor( } std::pair BenchmarkRunner::runConfiguration( - RunnableConfiguration &&RC, - const std::optional &DumpFile) const { + RunnableConfiguration &&RC, const std::optional &DumpFile, + std::optional BenchmarkProcessCPU) const { Benchmark &BenchmarkResult = RC.BenchmarkResult; object::OwningBinary &ObjectFile = RC.ObjectFile; @@ -665,7 +704,8 @@ std::pair BenchmarkRunner::runConfiguration( } Expected> Executor = - createFunctionExecutor(std::move(ObjectFile), RC.BenchmarkResult.Key); + createFunctionExecutor(std::move(ObjectFile), RC.BenchmarkResult.Key, + BenchmarkProcessCPU); if (!Executor) return {Executor.takeError(), std::move(BenchmarkResult)}; auto NewMeasurements = runMeasurements(**Executor); diff --git a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h index 9b4bb1d41149fe..e688b814d1c83d 100644 --- a/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h +++ b/llvm/tools/llvm-exegesis/lib/BenchmarkRunner.h @@ -68,7 +68,8 @@ class BenchmarkRunner { std::pair runConfiguration(RunnableConfiguration &&RC, - const std::optional &DumpFile) const; + const std::optional &DumpFile, + std::optional BenchmarkProcessCPU) const; // Scratch space to run instructions that touch memory. struct ScratchSpace { @@ -135,7 +136,8 @@ class BenchmarkRunner { Expected> createFunctionExecutor(object::OwningBinary Obj, - const BenchmarkKey &Key) const; + const BenchmarkKey &Key, + std::optional BenchmarkProcessCPU) const; }; } // namespace exegesis diff --git a/llvm/tools/llvm-exegesis/llvm-exegesis.cpp b/llvm/tools/llvm-exegesis/llvm-exegesis.cpp index e6a43cfc6db51c..546ec770a8d221 100644 --- a/llvm/tools/llvm-exegesis/llvm-exegesis.cpp +++ b/llvm/tools/llvm-exegesis/llvm-exegesis.cpp @@ -269,6 +269,11 @@ static cl::list ValidationCounters( "counter to validate benchmarking assumptions"), cl::CommaSeparated, cl::cat(BenchmarkOptions), ValidationEventOptions()); +static cl::opt BenchmarkProcessCPU( + "benchmark-process-cpu", + cl::desc("The CPU number that the benchmarking process should executon on"), + cl::cat(BenchmarkOptions), cl::init(-1)); + static ExitOnError ExitOnErr("llvm-exegesis error: "); // Helper function that logs the error(s) and exits. @@ -418,8 +423,12 @@ static void runBenchmarkConfigurations( std::optional DumpFile; if (DumpObjectToDisk.getNumOccurrences()) DumpFile = DumpObjectToDisk; + const std::optional BenchmarkCPU = + BenchmarkProcessCPU == -1 + ? std::nullopt + : std::optional(BenchmarkProcessCPU.getValue()); auto [Err, BenchmarkResult] = - Runner.runConfiguration(std::move(RC), DumpFile); + Runner.runConfiguration(std::move(RC), DumpFile, BenchmarkCPU); if (Err) { // Errors from executing the snippets are fine. // All other errors are a framework issue and should fail.