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
This commit is contained in:
Nicholas Mosier
2024-04-23 02:19:24 +00:00
committed by Ivana Mitrovic
parent 9f5c97c7fd
commit 30ea15009f
3 changed files with 237 additions and 79 deletions

View File

@@ -107,8 +107,9 @@ BaseKvmCPU::BaseKvmCPU(const BaseKvmCPUParams &params)
// If we use perf, we create new PerfKVMCounters
if (usePerf) {
hwCycles = std::unique_ptr<PerfKvmCounter>(new PerfKvmCounter());
hwInstructions = std::unique_ptr<PerfKvmCounter>(new PerfKvmCounter());
hwCycles = std::unique_ptr<PerfKvmCounter>(PerfKvmCounter::create());
hwInstructions =
std::unique_ptr<PerfKvmCounter>(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);

View File

@@ -35,6 +35,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <dirent.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
@@ -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<const SimplePerfKvmCounter &>(*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<const HybridPerfKvmCounter &>(*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

View File

@@ -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