From 30ea15009f72f017e14840bd35c8b8a0f6293610 Mon Sep 17 00:00:00 2001 From: Nicholas Mosier Date: Tue, 23 Apr 2024 02:19:24 +0000 Subject: [PATCH] cpu-kvm: Support perf counters on hybrid host architectures Fix #1064 by adding support for hardware performance counters on hybrid architectures like Intel Alder Lake. Hybrid architectures have multiple types of cores, each of which require the instantiation of a separate performance counter. The KVM CPU's PerfKvmCounter class was not aware of this, any only instantiated a single performance counter, implicitly bound to the P-core only. This meant that if gem5 ever ran on an E-core, the various hardware performance counters would not get updated properly, in some cases always zero (e.g., for the number of instructions executed). This patch adds support for hybrid host architectures as follows. First, we convert PerfKvmCounter into an abstract class, which has two concrete implementations: SimplePerfKvmCounter and HybridPerfKvmCounter. The former is used for non-hybrid architectures or for non-hardware performance counters and is functionally equivalent to the prior implementation of PerfKvmCounter. The latter is used for instantiating hardware performance counters (i.e., of type PERF_TYPE_HARDWARE) on hybrid host architectures. It does so by internally instantiating two SimplePerfKvmCounters, one for a P-core and one for an E-core. Upon read, it sums the results of reading the two internal counters. Change-Id: If64fcb0e2fcc1b3a6a37d77455c2b21e1fc81150 --- src/cpu/kvm/base.cc | 7 +- src/cpu/kvm/perfevent.cc | 162 ++++++++++++++++++++++++++++++++------- src/cpu/kvm/perfevent.hh | 147 +++++++++++++++++++++++------------ 3 files changed, 237 insertions(+), 79 deletions(-) diff --git a/src/cpu/kvm/base.cc b/src/cpu/kvm/base.cc index eaa771d8cf..8eb625c8bc 100644 --- a/src/cpu/kvm/base.cc +++ b/src/cpu/kvm/base.cc @@ -107,8 +107,9 @@ BaseKvmCPU::BaseKvmCPU(const BaseKvmCPUParams ¶ms) // If we use perf, we create new PerfKVMCounters if (usePerf) { - hwCycles = std::unique_ptr(new PerfKvmCounter()); - hwInstructions = std::unique_ptr(new PerfKvmCounter()); + hwCycles = std::unique_ptr(PerfKvmCounter::create()); + hwInstructions = + std::unique_ptr(PerfKvmCounter::create()); } else { inform("Using KVM CPU without perf. The stats related to the number " "of cycles and instructions executed by the KVM CPU will not " @@ -1409,7 +1410,7 @@ BaseKvmCPU::setupInstCounter(uint64_t period) assert(hwCycles->attached()); hwInstructions->attach(cfgInstructions, 0, // TID (0 => currentThread) - *hwCycles); + hwCycles.get()); if (period) hwInstructions->enableSignals(KVM_KICK_SIGNAL); diff --git a/src/cpu/kvm/perfevent.cc b/src/cpu/kvm/perfevent.cc index c5e33abf82..44d956f40f 100644 --- a/src/cpu/kvm/perfevent.cc +++ b/src/cpu/kvm/perfevent.cc @@ -35,6 +35,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include #include #include @@ -67,33 +68,51 @@ PerfKvmCounterConfig::~PerfKvmCounterConfig() { } - -PerfKvmCounter::PerfKvmCounter(PerfKvmCounterConfig &config, pid_t tid) - : fd(-1), ringBuffer(NULL), pageSize(-1) -{ - attach(config, tid, -1); -} - -PerfKvmCounter::PerfKvmCounter(PerfKvmCounterConfig &config, - pid_t tid, const PerfKvmCounter &parent) - : fd(-1), ringBuffer(NULL), pageSize(-1) -{ - attach(config, tid, parent); -} - PerfKvmCounter::PerfKvmCounter() +{ +} + +PerfKvmCounter * +PerfKvmCounter::create() +{ + // Check if we're running on a hybrid host architecture. Linux exposes + // this via sysfs. If the directory /sys/devices/cpu exists, then we are + // running on a regular architecture. Otherwise, /sys/devices/cpu_core and + // /sys/devices/cpu_atom should exist. For simplicity, we use the + // existence of /sys/devices/cpu_atom to indicate a hybrid host + // architecture. + const char *atom_path = "/sys/devices/cpu_atom"; + if (DIR *atom_dir = opendir(atom_path)) { + closedir(atom_dir); + + // Since we're running on a hybrid architecture, use a hybrid + // performance counter. This uses two 'physical' performance counters + // to implement a 'logical' one which is the sum of the two. + return new HybridPerfKvmCounter(); + } else { + if (errno != ENOENT) + warn("Unexpected error code from opendir(%s): %s\n", + atom_path, std::strerror(errno)); + + // We're running on a regular architecture, so use a regular + // performance counter. + return new SimplePerfKvmCounter(); + } +} + +SimplePerfKvmCounter::SimplePerfKvmCounter() : fd(-1), ringBuffer(NULL), pageSize(-1) { } -PerfKvmCounter::~PerfKvmCounter() +SimplePerfKvmCounter::~SimplePerfKvmCounter() { if (attached()) detach(); } void -PerfKvmCounter::detach() +SimplePerfKvmCounter::detach() { assert(attached()); @@ -107,35 +126,35 @@ PerfKvmCounter::detach() } void -PerfKvmCounter::start() +SimplePerfKvmCounter::start() { if (ioctl(PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP) == -1) panic("KVM: Failed to enable performance counters (%i)\n", errno); } void -PerfKvmCounter::stop() +SimplePerfKvmCounter::stop() { if (ioctl(PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP) == -1) panic("KVM: Failed to disable performance counters (%i)\n", errno); } void -PerfKvmCounter::period(uint64_t period) +SimplePerfKvmCounter::period(uint64_t period) { if (ioctl(PERF_EVENT_IOC_PERIOD, &period) == -1) panic("KVM: Failed to set period of performance counter (%i)\n", errno); } void -PerfKvmCounter::refresh(int refresh) +SimplePerfKvmCounter::refresh(int refresh) { if (ioctl(PERF_EVENT_IOC_REFRESH, refresh) == -1) panic("KVM: Failed to refresh performance counter (%i)\n", errno); } uint64_t -PerfKvmCounter::read() const +SimplePerfKvmCounter::read() const { uint64_t value; @@ -144,7 +163,7 @@ PerfKvmCounter::read() const } void -PerfKvmCounter::enableSignals(pid_t tid, int signal) +SimplePerfKvmCounter::enableSignals(pid_t tid, int signal) { struct f_owner_ex sigowner; @@ -159,11 +178,15 @@ PerfKvmCounter::enableSignals(pid_t tid, int signal) } void -PerfKvmCounter::attach(PerfKvmCounterConfig &config, - pid_t tid, int group_fd) +SimplePerfKvmCounter::attach(PerfKvmCounterConfig &config, + pid_t tid, const PerfKvmCounter *parent) { assert(!attached()); + int group_fd = -1; + if (parent) + group_fd = dynamic_cast(*parent).fd; + fd = syscall(__NR_perf_event_open, &config.attr, tid, -1, // CPU (-1 => Any CPU that the task happens to run on) @@ -201,7 +224,7 @@ PerfKvmCounter::sysGettid() } void -PerfKvmCounter::mmapPerf(int pages) +SimplePerfKvmCounter::mmapPerf(int pages) { assert(attached()); assert(ringBuffer == NULL); @@ -224,21 +247,21 @@ PerfKvmCounter::mmapPerf(int pages) } int -PerfKvmCounter::fcntl(int cmd, long p1) +SimplePerfKvmCounter::fcntl(int cmd, long p1) { assert(attached()); return ::fcntl(fd, cmd, p1); } int -PerfKvmCounter::ioctl(int request, long p1) +SimplePerfKvmCounter::ioctl(int request, long p1) { assert(attached()); return ::ioctl(fd, request, p1); } void -PerfKvmCounter::read(void *buf, size_t size) const +SimplePerfKvmCounter::read(void *buf, size_t size) const { char *_buf = (char *)buf; size_t _size = size; @@ -265,4 +288,87 @@ PerfKvmCounter::read(void *buf, size_t size) const } while (_size); } +void +HybridPerfKvmCounter::attach(PerfKvmCounterConfig &config, pid_t tid, + const PerfKvmCounter *parent) +{ + // We should only be using hybrid performance counters for hardware + // events. + assert(config.attr.type == PERF_TYPE_HARDWARE); + + const SimplePerfKvmCounter *parent_core_counter = nullptr; + const SimplePerfKvmCounter *parent_atom_counter = nullptr; + if (parent) { + const HybridPerfKvmCounter &hybrid_parent = + dynamic_cast(*parent); + parent_core_counter = &hybrid_parent.coreCounter; + parent_atom_counter = &hybrid_parent.atomCounter; + } + + PerfKvmCounterConfig config_core = config; + config_core.attr.config |= ConfigCore; + coreCounter.attach(config_core, tid, parent_core_counter); + + PerfKvmCounterConfig config_atom = config; + config_atom.attr.config |= ConfigAtom; + atomCounter.attach(config_atom, tid, parent_atom_counter); +} + +void +HybridPerfKvmCounter::detach() +{ + coreCounter.detach(); + atomCounter.detach(); +} + +bool +HybridPerfKvmCounter::attached() const +{ + assert(coreCounter.attached() == atomCounter.attached()); + return coreCounter.attached(); +} + +void +HybridPerfKvmCounter::start() +{ + coreCounter.start(); + atomCounter.start(); +} + +void +HybridPerfKvmCounter::stop() +{ + coreCounter.stop(); + atomCounter.stop(); +} + +void +HybridPerfKvmCounter::period(uint64_t period) +{ + coreCounter.period(period); + atomCounter.period(period); +} + +void +HybridPerfKvmCounter::refresh(int refresh) +{ + coreCounter.refresh(refresh); + atomCounter.refresh(refresh); +} + +uint64_t +HybridPerfKvmCounter::read() const +{ + // To get the logical counter value, we simply sum the individual physical + // counter values. + return coreCounter.read() + atomCounter.read(); +} + +void +HybridPerfKvmCounter::enableSignals(pid_t tid, int signal) +{ + coreCounter.enableSignals(tid, signal); + atomCounter.enableSignals(tid, signal); +} + } // namespace gem5 diff --git a/src/cpu/kvm/perfevent.hh b/src/cpu/kvm/perfevent.hh index 70a246f7e9..5424629074 100644 --- a/src/cpu/kvm/perfevent.hh +++ b/src/cpu/kvm/perfevent.hh @@ -170,46 +170,23 @@ class PerfKvmCounterConfig */ class PerfKvmCounter { -public: - /** - * Create and attach a new counter group. - * - * @param config Counter configuration - * @param tid Thread to sample (0 indicates current thread) - */ - PerfKvmCounter(PerfKvmCounterConfig &config, pid_t tid); - /** - * Create and attach a new counter and make it a member of an - * exist counter group. - * - * @param config Counter configuration - * @param tid Thread to sample (0 indicates current thread) - * @param parent Group leader - */ - PerfKvmCounter(PerfKvmCounterConfig &config, - pid_t tid, const PerfKvmCounter &parent); - /** - * Create a new counter, but don't attach it. - */ + protected: + /** Don't create directly; use PerfKvmCounter::create(). */ PerfKvmCounter(); - ~PerfKvmCounter(); + public: + virtual ~PerfKvmCounter() = default; /** - * Attach a counter. - * - * @note This operation is only supported if the counter isn't - * already attached. - * - * @param config Counter configuration - * @param tid Thread to sample (0 indicates current thread) + * Create a new performance counter. + * This automatically selects the appropriate implementation of + * PerfKvmCounter, depending on whether the host has a hybrid architecture + * (rare case) or not (common case). */ - void attach(PerfKvmCounterConfig &config, pid_t tid) { - attach(config, tid, -1); - } + static PerfKvmCounter *create(); /** - * Attach a counter and make it a member of an existing counter + * Attach a counter and optionally make it a member of an existing counter * group. * * @note This operation is only supported if the counter isn't @@ -217,18 +194,16 @@ public: * * @param config Counter configuration * @param tid Thread to sample (0 indicates current thread) - * @param parent Group leader + * @param parent Group leader (nullptr indicates no group leader) */ - void attach(PerfKvmCounterConfig &config, - pid_t tid, const PerfKvmCounter &parent) { - attach(config, tid, parent.fd); - } + virtual void attach(PerfKvmCounterConfig &config, + pid_t tid, const PerfKvmCounter *parent = nullptr) = 0; /** Detach a counter from PerfEvent. */ - void detach(); + virtual void detach() = 0; /** Check if a counter is attached. */ - bool attached() const { return fd != -1; } + virtual bool attached() const = 0; /** * Start counting. @@ -236,7 +211,7 @@ public: * @note If this counter is a group leader, it will start the * entire group. */ - void start(); + virtual void start() = 0; /** * Stop counting. @@ -244,7 +219,7 @@ public: * @note If this counter is a group leader, it will stop the * entire group. */ - void stop(); + virtual void stop() = 0; /** * Update the period of an overflow counter. @@ -262,7 +237,7 @@ public: * * @param period Overflow period in events */ - void period(uint64_t period); + virtual void period(uint64_t period) = 0; /** * Enable a counter for a fixed number of events. @@ -276,12 +251,12 @@ public: * @param refresh Number of overflows before disabling the * counter. */ - void refresh(int refresh); + virtual void refresh(int refresh) = 0; /** * Read the current value of a counter. */ - uint64_t read() const; + virtual uint64_t read() const = 0; /** * Enable signal delivery to a thread on counter overflow. @@ -289,7 +264,7 @@ public: * @param tid Thread to deliver signal to * @param signal Signal to send upon overflow */ - void enableSignals(pid_t tid, int signal); + virtual void enableSignals(pid_t tid, int signal) = 0; /** * Enable signal delivery on counter overflow. Identical to @@ -306,15 +281,43 @@ private: // Disallow assignment PerfKvmCounter &operator=(const PerfKvmCounter &that); - void attach(PerfKvmCounterConfig &config, pid_t tid, int group_fd); - /** * Get the TID of the current thread. * * @return Current thread's TID */ pid_t sysGettid(); +}; +/** + * An instance of a single physical performance counter. + * In almost all cases, this is the PerfKvmCounter implementation to use. The + * only situation in which you should _not_ use this counter is for creating + a hardware event on a hybrid host architecture. In that case, use + * a HybridPerfKvmCounter. + */ +class SimplePerfKvmCounter final : public PerfKvmCounter +{ + private: + /** Don't create directly; use PerfKvmCounter::create(). */ + SimplePerfKvmCounter(); + + public: + ~SimplePerfKvmCounter(); + + void attach(PerfKvmCounterConfig &config, + pid_t tid, const PerfKvmCounter *parent) override; + + void detach() override; + bool attached() const override { return fd != -1; } + void start() override; + void stop() override; + void period(uint64_t period) override; + void refresh(int refresh) override; + uint64_t read() const override; + void enableSignals(pid_t tid, int signal) override; + + private: /** * MMAP the PerfEvent file descriptor. * @@ -379,6 +382,54 @@ private: /** Cached host page size */ long pageSize; + + friend class PerfKvmCounter; + friend class HybridPerfKvmCounter; +}; + +/** + * Implements a single logical performance counter using two + * physical performance counters (i.e., SimplePerfKvmCounter). + * Use this for hardware counters (i.e., of type PERF_TYPE_HARDWARE) + * when running on a hybrid host architecture. Such architectures have + * multiple types of cores, each of which require their own individual + * performance counter. + */ +class HybridPerfKvmCounter : public PerfKvmCounter +{ + private: + /** Don't create directly; use PerfKvmCounter::create(). */ + HybridPerfKvmCounter() = default; + + public: + void attach(PerfKvmCounterConfig &config, pid_t tid, + const PerfKvmCounter *parent) override; + void detach() override; + bool attached() const override; + void start() override; + void stop() override; + void period(uint64_t period) override; + void refresh(int refresh) override; + uint64_t read() const override; + void enableSignals(pid_t pid, int signal) override; + + private: + SimplePerfKvmCounter coreCounter; + SimplePerfKvmCounter atomCounter; + + using Config = decltype(perf_event_attr::config); + + /** @{ */ + /** + * These constants for specifying core vs. atom events are taken from + * Linux perf's documentation (tools/perf/Documentation/intel-hybrid.txt + * in the linux source tree). + */ + static inline constexpr Config ConfigCore = 0x4UL << 32; + static inline constexpr Config ConfigAtom = 0x8UL << 32; + /** @} */ + + friend class PerfKvmCounter; }; } // namespace gem5