|
| 1 | +// Minimal perf_event_open wrapper: counts userspace HW events for a child process. |
| 2 | +// No `perf` binary and no root needed when /proc/sys/kernel/perf_event_paranoid <= 2 |
| 3 | +// (we set exclude_kernel). Works on WSL2 when the guest exposes the PMU |
| 4 | +// (check: ls /sys/devices/cpu/events should list `instructions`, `cycles`, ...). |
| 5 | +// |
| 6 | +// gcc -O2 -o bench/perfcount bench/perfcount.c |
| 7 | +// bench/perfcount <cmd> [args...] |
| 8 | +// |
| 9 | +// Prints one line to stderr: |
| 10 | +// PERF instructions=<n> cycles=<n> branch-misses=<n> cache-misses=<n> |
| 11 | +// |
| 12 | +// Instruction counts are frequency-independent, so they're stable on battery, |
| 13 | +// under load, and across machines -- far better than wall-clock for resolving |
| 14 | +// a sub-percent engine cost. |
| 15 | +#define _GNU_SOURCE |
| 16 | +#include <stdio.h> |
| 17 | +#include <stdlib.h> |
| 18 | +#include <unistd.h> |
| 19 | +#include <string.h> |
| 20 | +#include <errno.h> |
| 21 | +#include <sys/wait.h> |
| 22 | +#include <sys/ioctl.h> |
| 23 | +#include <linux/perf_event.h> |
| 24 | +#include <asm/unistd.h> |
| 25 | + |
| 26 | +static long perf_open(struct perf_event_attr *a, pid_t pid) { |
| 27 | + return syscall(__NR_perf_event_open, a, pid, -1, -1, 0); |
| 28 | +} |
| 29 | + |
| 30 | +static int mkcounter(unsigned type, unsigned long long config, pid_t pid) { |
| 31 | + struct perf_event_attr a; |
| 32 | + memset(&a, 0, sizeof(a)); |
| 33 | + a.type = type; |
| 34 | + a.size = sizeof(a); |
| 35 | + a.config = config; |
| 36 | + a.disabled = 1; // start off... |
| 37 | + a.enable_on_exec = 1; // ...auto-enable when the child execs |
| 38 | + a.exclude_kernel = 1; // userspace only (allowed at paranoid=2) |
| 39 | + a.exclude_hv = 1; |
| 40 | + a.inherit = 1; // follow into the exec'd image |
| 41 | + int fd = (int) perf_open(&a, pid); |
| 42 | + if (fd < 0) fprintf(stderr, "perf_open(type=%u,config=%llu) failed: %s\n", type, config, strerror(errno)); |
| 43 | + return fd; |
| 44 | +} |
| 45 | + |
| 46 | +static long long readval(int fd) { |
| 47 | + long long v = -1; |
| 48 | + if (fd >= 0 && read(fd, &v, sizeof(v)) != sizeof(v)) v = -1; |
| 49 | + return v; |
| 50 | +} |
| 51 | + |
| 52 | +int main(int argc, char **argv) { |
| 53 | + if (argc < 2) { fprintf(stderr, "usage: %s <cmd> [args...]\n", argv[0]); return 2; } |
| 54 | + |
| 55 | + int pfd[2]; |
| 56 | + if (pipe(pfd) != 0) { perror("pipe"); return 3; } |
| 57 | + |
| 58 | + pid_t pid = fork(); |
| 59 | + if (pid < 0) { perror("fork"); return 3; } |
| 60 | + |
| 61 | + if (pid == 0) { |
| 62 | + // child: wait for parent to arm counters, then exec |
| 63 | + close(pfd[1]); |
| 64 | + char b; if (read(pfd[0], &b, 1) < 0) _exit(126); |
| 65 | + close(pfd[0]); |
| 66 | + execvp(argv[1], &argv[1]); |
| 67 | + perror("execvp"); |
| 68 | + _exit(127); |
| 69 | + } |
| 70 | + |
| 71 | + // parent: arm counters against the (still-blocked) child |
| 72 | + close(pfd[0]); |
| 73 | + int f_ins = mkcounter(PERF_TYPE_HARDWARE, PERF_COUNT_HW_INSTRUCTIONS, pid); |
| 74 | + int f_cyc = mkcounter(PERF_TYPE_HARDWARE, PERF_COUNT_HW_CPU_CYCLES, pid); |
| 75 | + int f_brm = mkcounter(PERF_TYPE_HARDWARE, PERF_COUNT_HW_BRANCH_MISSES, pid); |
| 76 | + int f_chm = mkcounter(PERF_TYPE_HARDWARE, PERF_COUNT_HW_CACHE_MISSES, pid); |
| 77 | + |
| 78 | + // release the child |
| 79 | + char go = 1; if (write(pfd[1], &go, 1) < 0) { /* child will EOF and exit */ } |
| 80 | + close(pfd[1]); |
| 81 | + |
| 82 | + int status = 0; |
| 83 | + waitpid(pid, &status, 0); |
| 84 | + |
| 85 | + fprintf(stderr, "PERF instructions=%lld cycles=%lld branch-misses=%lld cache-misses=%lld\n", |
| 86 | + readval(f_ins), readval(f_cyc), readval(f_brm), readval(f_chm)); |
| 87 | + |
| 88 | + if (WIFEXITED(status)) return WEXITSTATUS(status); |
| 89 | + return 1; |
| 90 | +} |
0 commit comments