feat: vm-native setup, verify and gpu tooling with acpi and bench
This commit is contained in:
295
bench/timerprobe.c
Normal file
295
bench/timerprobe.c
Normal file
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* timerprobe - replicates VMAware's TIMER check so both of its ratios can be
|
||||
* measured directly, without needing a debug build of VMAware itself.
|
||||
*
|
||||
* Build: x86_64-w64-mingw32-gcc -O2 -o timerprobe.exe timerprobe.c
|
||||
*
|
||||
* The check uses a software clock: a second thread on another core spins
|
||||
* incrementing a counter on its own cache line, and the measuring thread reads
|
||||
* that line either side of the operation. A "tick" is one observed increment,
|
||||
* so the unit is a cross-core cache-line bounce, and there is no TSC to lie to.
|
||||
*
|
||||
* Detector 1 times a CPUID against eight LFENCEs. Detector 2 times a hardware
|
||||
* TF single-step #DB against ZwRaiseException raising EXCEPTION_SINGLE_STEP in
|
||||
* software. Both are ratio >= 2.5 -> detected, and they OR together.
|
||||
*
|
||||
* Detector 2 is reproduced mechanism for mechanism: a __C_specific_handler
|
||||
* scope table with a filter funclet (MSVC's __try without the syntax), ZwRaiseException from ntdll with a RtlCaptureContext context, and
|
||||
* EXCEPTION_EXECUTE_HANDLER on the software side so RtlUnwindEx runs. An
|
||||
* earlier version used a vectored handler and RaiseException, which never
|
||||
* enters the kernel on the software side and so measured a different ratio.
|
||||
*
|
||||
* Usage: timerprobe.exe [measure_cpu] [counter_cpu]
|
||||
* with no arguments it sweeps every counter placement for detector 1,
|
||||
* then runs detector 2 once.
|
||||
*/
|
||||
|
||||
#include <windows.h>
|
||||
#include <winternl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <intrin.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TRIALS 5
|
||||
#define BATCH 800
|
||||
|
||||
typedef unsigned long long u64;
|
||||
typedef NTSTATUS (NTAPI *zw_raise_exception_fn)(PEXCEPTION_RECORD, PCONTEXT, BOOLEAN);
|
||||
|
||||
/* the counter thread's line, alone on its own 64-byte line */
|
||||
static volatile u64 g_counter __attribute__((aligned(64)));
|
||||
static volatile LONG g_stop __attribute__((aligned(64)));
|
||||
|
||||
static int counter_cpu;
|
||||
static zw_raise_exception_fn zw_raise_exception;
|
||||
|
||||
static DWORD WINAPI counter_thread(LPVOID p)
|
||||
{
|
||||
(void)p;
|
||||
SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1 << counter_cpu);
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
SetThreadPriorityBoost(GetCurrentThread(), TRUE);
|
||||
u64 local = 0;
|
||||
while (!g_stop) {
|
||||
local++;
|
||||
g_counter = local;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmp_u64(const void *a, const void *b)
|
||||
{
|
||||
u64 x = *(const u64 *)a, y = *(const u64 *)b;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
/* interquartile mean of the middle 50%, as VMAware's calculate_latency does */
|
||||
static double iqm(u64 *v, size_t n)
|
||||
{
|
||||
qsort(v, n, sizeof(u64), cmp_u64);
|
||||
size_t lo = n / 4, hi = n - n / 4;
|
||||
if (hi <= lo) return (double)v[n / 2];
|
||||
double s = 0;
|
||||
for (size_t i = lo; i < hi; i++) s += (double)v[i];
|
||||
return s / (double)(hi - lo);
|
||||
}
|
||||
|
||||
static volatile int burn_sink;
|
||||
|
||||
static void burn(void) /* stops a hypervisor predicting the sample window */
|
||||
{
|
||||
int rounds = 64 + (rand() & 0x7FF);
|
||||
for (int i = 0; i < rounds; i++) burn_sink += i;
|
||||
}
|
||||
|
||||
static inline u64 sync_edge(void)
|
||||
{
|
||||
u64 s = g_counter;
|
||||
while (g_counter == s) { }
|
||||
return g_counter;
|
||||
}
|
||||
|
||||
/* one batch: time `op` against the cross-core counter */
|
||||
static double window(int op, u64 *buf)
|
||||
{
|
||||
for (size_t i = 0; i < BATCH; i++) {
|
||||
u64 start, end;
|
||||
int regs[4];
|
||||
|
||||
start = sync_edge();
|
||||
if (op == 0) {
|
||||
__cpuid(regs, 0);
|
||||
} else {
|
||||
_mm_lfence(); _mm_lfence(); _mm_lfence(); _mm_lfence();
|
||||
_mm_lfence(); _mm_lfence(); _mm_lfence(); _mm_lfence();
|
||||
}
|
||||
end = g_counter;
|
||||
buf[i] = end - start;
|
||||
burn();
|
||||
}
|
||||
return iqm(buf, BATCH);
|
||||
}
|
||||
|
||||
static double best_of_trials(int op, u64 *buf)
|
||||
{
|
||||
double best = 1e18;
|
||||
for (int t = 0; t < TRIALS; t++) {
|
||||
double v = window(op, buf);
|
||||
if (v < best) best = v;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Detector 2, the VMAware shape.
|
||||
*
|
||||
* Hardware side: set TF, execute one NOP, take #DB, the filter clears TF and
|
||||
* continues - one trap in, one NtContinue out. Software side: capture a
|
||||
* context and hand it to ZwRaiseException as a first-chance single-step, the
|
||||
* filter executes the handler - one syscall in, one RtlUnwindEx out.
|
||||
*
|
||||
* mingw's __try1 uses one fixed label pair per file, so this is the same
|
||||
* directive sequence with a per-scope label. Each side is its own noinline
|
||||
* function. The trailing NOP keeps the trap RIP inside the scope: a TF trap
|
||||
* reports the address of the instruction after the one that completed.
|
||||
* ------------------------------------------------------------------------- */
|
||||
#define SEH_TRY(filter, id) __asm__ __volatile__( \
|
||||
".Lseh_start_" #id ":\n\t" \
|
||||
".seh_handler __C_specific_handler, @except\n\t" \
|
||||
".seh_handlerdata\n\t.long 1\n\t" \
|
||||
".rva .Lseh_start_" #id ", .Lseh_end_" #id ", " #filter ", .Lseh_end_" #id "\n\t" \
|
||||
".text")
|
||||
#define SEH_END(id) __asm__ __volatile__("nop\n.Lseh_end_" #id ": nop")
|
||||
|
||||
static volatile long g_db_hits;
|
||||
|
||||
static __attribute__((used)) LONG NTAPI db_filter(EXCEPTION_POINTERS *ep, ULONG64 frame)
|
||||
{
|
||||
(void)frame;
|
||||
if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
ep->ContextRecord->EFlags &= ~0x100u;
|
||||
g_db_hits++;
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
static __attribute__((used)) LONG NTAPI api_filter(EXCEPTION_POINTERS *ep, ULONG64 frame)
|
||||
{
|
||||
(void)frame;
|
||||
return ep->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP
|
||||
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
static __attribute__((noinline)) void execute_db(void)
|
||||
{
|
||||
SEH_TRY(db_filter, db);
|
||||
__asm__ __volatile__("pushfq\n\torq $0x100,(%%rsp)\n\tpopfq\n\tnop\n\tnop"
|
||||
::: "memory", "cc");
|
||||
SEH_END(db);
|
||||
}
|
||||
|
||||
static __attribute__((noinline)) void nt_raise_exception(EXCEPTION_RECORD *er, CONTEXT *ctx,
|
||||
volatile int *flag)
|
||||
{
|
||||
SEH_TRY(api_filter, api);
|
||||
RtlCaptureContext(ctx);
|
||||
*flag = 1;
|
||||
zw_raise_exception(er, ctx, TRUE);
|
||||
SEH_END(api);
|
||||
}
|
||||
|
||||
static void exc_window(u64 *db, u64 *api)
|
||||
{
|
||||
for (size_t i = 0; i < BATCH; i++) {
|
||||
u64 pre, post;
|
||||
|
||||
pre = sync_edge();
|
||||
execute_db();
|
||||
post = g_counter;
|
||||
db[i] = post - pre;
|
||||
|
||||
volatile int flag = 0;
|
||||
CONTEXT ctx;
|
||||
memset(&ctx, 0, sizeof ctx);
|
||||
ctx.ContextFlags = CONTEXT_FULL;
|
||||
EXCEPTION_RECORD er;
|
||||
memset(&er, 0, sizeof er);
|
||||
er.ExceptionCode = EXCEPTION_SINGLE_STEP;
|
||||
|
||||
pre = sync_edge();
|
||||
nt_raise_exception(&er, &ctx, &flag);
|
||||
post = g_counter;
|
||||
api[i] = post - pre;
|
||||
burn();
|
||||
}
|
||||
}
|
||||
|
||||
static void run_exception(int mcpu, int ccpu)
|
||||
{
|
||||
counter_cpu = ccpu; g_stop = 0; g_counter = 0;
|
||||
HANDLE h = CreateThread(NULL, 0, counter_thread, NULL, 0, NULL);
|
||||
SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1 << mcpu);
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
SetThreadPriorityBoost(GetCurrentThread(), TRUE);
|
||||
Sleep(80);
|
||||
|
||||
u64 *db = malloc(BATCH * sizeof(u64)), *api = malloc(BATCH * sizeof(u64));
|
||||
double best_db = 1e18, best_api = 1e18;
|
||||
for (int t = 0; t < TRIALS; t++) {
|
||||
exc_window(db, api);
|
||||
double v = iqm(db, BATCH); if (v < best_db) best_db = v;
|
||||
v = iqm(api, BATCH); if (v < best_api) best_api = v;
|
||||
}
|
||||
free(db); free(api);
|
||||
|
||||
g_stop = 1; WaitForSingleObject(h, 2000); CloseHandle(h);
|
||||
|
||||
double ratio = best_api > 0 ? best_db / best_api : 0;
|
||||
printf("exception db=%8.1f api=%8.1f ratio=%7.3f %s (db traps seen: %ld)\n",
|
||||
best_db, best_api, ratio, ratio >= 2.5 ? "DETECTED" : "pass", g_db_hits);
|
||||
}
|
||||
|
||||
static void run(int mcpu, int ccpu, int verbose)
|
||||
{
|
||||
counter_cpu = ccpu;
|
||||
g_stop = 0;
|
||||
g_counter = 0;
|
||||
|
||||
HANDLE h = CreateThread(NULL, 0, counter_thread, NULL, 0, NULL);
|
||||
if (!h) { printf("thread failed\n"); return; }
|
||||
|
||||
SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1 << mcpu);
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
SetThreadPriorityBoost(GetCurrentThread(), TRUE);
|
||||
Sleep(80); /* let the counter get going */
|
||||
|
||||
u64 *buf = malloc(BATCH * sizeof(u64));
|
||||
double cpuid_w = best_of_trials(0, buf);
|
||||
double ref_w = best_of_trials(1, buf);
|
||||
free(buf);
|
||||
|
||||
g_stop = 1;
|
||||
WaitForSingleObject(h, 2000);
|
||||
CloseHandle(h);
|
||||
|
||||
double ratio = ref_w > 0 ? cpuid_w / ref_w : 0;
|
||||
if (verbose)
|
||||
printf("measure=%-3d counter=%-3d cpuid=%8.1f lfence=%8.1f ratio=%7.3f %s\n",
|
||||
mcpu, ccpu, cpuid_w, ref_w, ratio, ratio >= 2.5 ? "DETECTED" : "pass");
|
||||
else
|
||||
printf("counter=%-3d ratio=%7.3f (cpuid %.0f / lfence %.0f) %s\n",
|
||||
ccpu, ratio, cpuid_w, ref_w, ratio >= 2.5 ? "DETECTED" : "pass");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
SYSTEM_INFO si; GetSystemInfo(&si);
|
||||
int n = (int)si.dwNumberOfProcessors;
|
||||
SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS);
|
||||
|
||||
zw_raise_exception = (zw_raise_exception_fn)(void *)
|
||||
GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwRaiseException");
|
||||
if (!zw_raise_exception) { printf("no ZwRaiseException\n"); return 1; }
|
||||
|
||||
printf("# timerprobe cpus=%d threshold=2.5\n", n);
|
||||
|
||||
if (argc >= 3) {
|
||||
run(atoi(argv[1]), atoi(argv[2]), 1);
|
||||
run_exception(atoi(argv[1]), atoi(argv[2]));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* VMAware picks the counter core itself, biased toward the same L3 as the
|
||||
* measuring thread. Sweep every placement to show the best and the worst
|
||||
* case rather than the one it happened to choose.
|
||||
*/
|
||||
printf("detector 1, instruction latency - counter placement swept:\n");
|
||||
for (int c = 1; c < n; c++) run(0, c, 0);
|
||||
|
||||
printf("\ndetector 2, exception latency:\n");
|
||||
run_exception(0, n > 1 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
491
bench/vmbench.c
Normal file
491
bench/vmbench.c
Normal file
@@ -0,0 +1,491 @@
|
||||
/*
|
||||
* vmbench - CPU, memory, scheduling and IO benchmarks for a Windows guest.
|
||||
*
|
||||
* Deliberately has no GPU component: this guest has no display adapter, so every
|
||||
* number here is a CPU, memory, scheduling or IO proxy for interactive and
|
||||
* CPU-bound workloads.
|
||||
*
|
||||
* Build: x86_64-w64-mingw32-gcc -O2 -o vmbench.exe vmbench.c
|
||||
*
|
||||
* Timing is QueryPerformanceCounter throughout. rdtsc is deliberately avoided:
|
||||
* TSC behaviour is one of the things the fidelity work changes, so timing with it
|
||||
* would measure the instrument.
|
||||
*/
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static double qpc_freq;
|
||||
|
||||
static double now(void)
|
||||
{
|
||||
LARGE_INTEGER c;
|
||||
QueryPerformanceCounter(&c);
|
||||
return (double)c.QuadPart / qpc_freq;
|
||||
}
|
||||
|
||||
static int cmp_double(const void *a, const void *b)
|
||||
{
|
||||
double x = *(const double *)a, y = *(const double *)b;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
static double pct(double *sorted, size_t n, double p)
|
||||
{
|
||||
double idx = p * (double)(n - 1);
|
||||
size_t lo = (size_t)idx;
|
||||
if (lo + 1 >= n) return sorted[n - 1];
|
||||
double frac = idx - (double)lo;
|
||||
return sorted[lo] + frac * (sorted[lo + 1] - sorted[lo]);
|
||||
}
|
||||
|
||||
static void pin(int cpu)
|
||||
{
|
||||
if (cpu >= 0) SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1 << cpu);
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
}
|
||||
|
||||
/* xorshift, used to build permutations without pulling in a real PRNG */
|
||||
static uint64_t rng_s = 0x243F6A8885A308D3ull;
|
||||
static uint64_t rng(void)
|
||||
{
|
||||
rng_s ^= rng_s << 13; rng_s ^= rng_s >> 7; rng_s ^= rng_s << 17;
|
||||
return rng_s;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- cpu ----- */
|
||||
/*
|
||||
* Two numbers, because latency-sensitive workloads need both:
|
||||
* latency - a serial dependency chain, one op per iteration, nothing to
|
||||
* overlap. This is what a single-threaded hot path looks like.
|
||||
* throughput- four independent chains the core can pipeline.
|
||||
* The volatile sink stops the optimiser deleting the whole loop.
|
||||
*/
|
||||
static volatile uint64_t sink_u64;
|
||||
static volatile double sink_f64;
|
||||
|
||||
static double cpu_int_latency(uint64_t iters)
|
||||
{
|
||||
uint64_t x = 1;
|
||||
double t0 = now();
|
||||
for (uint64_t i = 0; i < iters; i++) {
|
||||
x = x * 6364136223846793005ull + 1442695040888963407ull;
|
||||
x ^= x >> 29;
|
||||
}
|
||||
double t1 = now();
|
||||
sink_u64 = x;
|
||||
return (double)iters / (t1 - t0) / 1e6; /* Mops/s */
|
||||
}
|
||||
|
||||
static double cpu_int_throughput(uint64_t iters)
|
||||
{
|
||||
uint64_t a = 1, b = 2, c = 3, d = 4;
|
||||
double t0 = now();
|
||||
for (uint64_t i = 0; i < iters; i++) {
|
||||
a = a * 6364136223846793005ull + 1;
|
||||
b = b * 6364136223846793005ull + 2;
|
||||
c = c * 6364136223846793005ull + 3;
|
||||
d = d * 6364136223846793005ull + 4;
|
||||
}
|
||||
double t1 = now();
|
||||
sink_u64 = a ^ b ^ c ^ d;
|
||||
return (double)(iters * 4) / (t1 - t0) / 1e6;
|
||||
}
|
||||
|
||||
static double cpu_fp(uint64_t iters)
|
||||
{
|
||||
double a = 1.0000001, b = 1.0000002, c = 1.0000003, d = 1.0000004;
|
||||
double t0 = now();
|
||||
for (uint64_t i = 0; i < iters; i++) {
|
||||
a = a * 1.0000001 + 0.0000001;
|
||||
b = b * 1.0000002 + 0.0000002;
|
||||
c = c * 1.0000003 + 0.0000003;
|
||||
d = d * 1.0000004 + 0.0000004;
|
||||
}
|
||||
double t1 = now();
|
||||
sink_f64 = a + b + c + d;
|
||||
return (double)(iters * 4) / (t1 - t0) / 1e6;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ cpu_mt ----- */
|
||||
struct mt_arg { int cpu; uint64_t iters; double mops; };
|
||||
|
||||
static DWORD WINAPI mt_worker(LPVOID p)
|
||||
{
|
||||
struct mt_arg *a = (struct mt_arg *)p;
|
||||
pin(a->cpu);
|
||||
a->mops = cpu_int_throughput(a->iters);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void bench_cpu_mt(int nthreads, uint64_t iters)
|
||||
{
|
||||
struct mt_arg *args = calloc(nthreads, sizeof(*args));
|
||||
HANDLE *th = calloc(nthreads, sizeof(*th));
|
||||
for (int i = 0; i < nthreads; i++) {
|
||||
args[i].cpu = i;
|
||||
args[i].iters = iters;
|
||||
th[i] = CreateThread(NULL, 0, mt_worker, &args[i], 0, NULL);
|
||||
}
|
||||
WaitForMultipleObjects(nthreads, th, TRUE, INFINITE);
|
||||
double total = 0;
|
||||
for (int i = 0; i < nthreads; i++) { total += args[i].mops; CloseHandle(th[i]); }
|
||||
printf("cpu_mt threads=%d aggregate_Mops=%.1f per_thread_avg=%.1f\n",
|
||||
nthreads, total, total / nthreads);
|
||||
free(args); free(th);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- memlat ---- */
|
||||
/*
|
||||
* Pointer chase over a random single cycle, one node per 64-byte line so each
|
||||
* step is exactly one cache miss at the level being measured. A strided walk
|
||||
* would be prefetched and would measure bandwidth, not latency.
|
||||
*/
|
||||
struct node { struct node *next; char pad[56]; };
|
||||
|
||||
static double memlat(size_t bytes, uint64_t steps)
|
||||
{
|
||||
size_t n = bytes / sizeof(struct node);
|
||||
if (n < 16) n = 16;
|
||||
struct node *buf = _aligned_malloc(n * sizeof(struct node), 64);
|
||||
if (!buf) return -1;
|
||||
memset(buf, 0, n * sizeof(struct node));
|
||||
|
||||
size_t *perm = malloc(n * sizeof(size_t));
|
||||
for (size_t i = 0; i < n; i++) perm[i] = i;
|
||||
for (size_t i = n - 1; i > 0; i--) { /* Fisher-Yates */
|
||||
size_t j = (size_t)(rng() % (i + 1));
|
||||
size_t t = perm[i]; perm[i] = perm[j]; perm[j] = t;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) /* link into one cycle */
|
||||
buf[perm[i]].next = &buf[perm[(i + 1) % n]];
|
||||
free(perm);
|
||||
|
||||
struct node *p = buf;
|
||||
for (uint64_t i = 0; i < n * 2; i++) p = p->next; /* warm */
|
||||
|
||||
double t0 = now();
|
||||
for (uint64_t i = 0; i < steps; i++) p = p->next;
|
||||
double t1 = now();
|
||||
sink_u64 = (uint64_t)(uintptr_t)p;
|
||||
|
||||
_aligned_free(buf);
|
||||
return (t1 - t0) / (double)steps * 1e9; /* ns per access */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- membw ----- */
|
||||
static double membw_read(size_t bytes)
|
||||
{
|
||||
size_t n = bytes / sizeof(uint64_t);
|
||||
uint64_t *b = _aligned_malloc(n * sizeof(uint64_t), 64);
|
||||
if (!b) return -1;
|
||||
for (size_t i = 0; i < n; i++) b[i] = i;
|
||||
uint64_t s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
||||
double t0 = now();
|
||||
for (size_t i = 0; i + 3 < n; i += 4) {
|
||||
s0 += b[i]; s1 += b[i+1]; s2 += b[i+2]; s3 += b[i+3];
|
||||
}
|
||||
double t1 = now();
|
||||
sink_u64 = s0 + s1 + s2 + s3;
|
||||
_aligned_free(b);
|
||||
return (double)bytes / (t1 - t0) / 1e9; /* GB/s */
|
||||
}
|
||||
|
||||
static double membw_copy(size_t bytes)
|
||||
{
|
||||
size_t half = bytes / 2;
|
||||
char *a = _aligned_malloc(half, 64), *b = _aligned_malloc(half, 64);
|
||||
if (!a || !b) return -1;
|
||||
memset(a, 1, half); memset(b, 2, half);
|
||||
memcpy(b, a, half); /* warm */
|
||||
double t0 = now();
|
||||
memcpy(b, a, half);
|
||||
double t1 = now();
|
||||
sink_u64 = (uint64_t)b[0];
|
||||
_aligned_free(a); _aligned_free(b);
|
||||
return (double)(half * 2) / (t1 - t0) / 1e9;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- c2c ----- */
|
||||
/*
|
||||
* Core-to-core round trip. On a 7950X this is the number that separates a
|
||||
* same-CCD pair from a cross-CCD pair, which is the whole 16-vs-32 vCPU
|
||||
* argument, so it gets measured directly rather than assumed.
|
||||
*/
|
||||
static volatile LONG c2c_flag;
|
||||
static int c2c_cpu_b;
|
||||
static uint64_t c2c_iters;
|
||||
|
||||
static DWORD WINAPI c2c_worker(LPVOID p)
|
||||
{
|
||||
(void)p;
|
||||
pin(c2c_cpu_b);
|
||||
for (uint64_t i = 0; i < c2c_iters; i++) {
|
||||
while (InterlockedCompareExchange(&c2c_flag, 2, 1) != 1) YieldProcessor();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double c2c(int cpu_a, int cpu_b, uint64_t iters)
|
||||
{
|
||||
c2c_cpu_b = cpu_b; c2c_iters = iters; c2c_flag = 0;
|
||||
HANDLE h = CreateThread(NULL, 0, c2c_worker, NULL, 0, NULL);
|
||||
pin(cpu_a);
|
||||
Sleep(50);
|
||||
double t0 = now();
|
||||
for (uint64_t i = 0; i < iters; i++) {
|
||||
InterlockedExchange(&c2c_flag, 1);
|
||||
while (InterlockedCompareExchange(&c2c_flag, 0, 2) != 2) YieldProcessor();
|
||||
}
|
||||
double t1 = now();
|
||||
WaitForSingleObject(h, 5000);
|
||||
CloseHandle(h);
|
||||
return (t1 - t0) / (double)iters * 1e9; /* ns round trip */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ jitter ----- */
|
||||
/*
|
||||
* The scheduling-tail proxy, and the most important number in the suite.
|
||||
*
|
||||
* Spin reading the clock as fast as possible and record every gap between
|
||||
* consecutive reads. On an idle, well-behaved system the gaps are tiny and
|
||||
* uniform. Any gap far above the floor means this thread was NOT RUNNING -
|
||||
* preempted by the host, stalled behind a VMEXIT, or waiting on the emulator
|
||||
* thread. Mean throughput averages all of that away; the tail is what shows up
|
||||
* as hitching in an interactive session, so the output is percentiles, not an
|
||||
* average.
|
||||
*/
|
||||
static void jitter(int cpu, double seconds, size_t max_samples)
|
||||
{
|
||||
pin(cpu);
|
||||
double *gaps = malloc(max_samples * sizeof(double));
|
||||
if (!gaps) { printf("jitter alloc failed\n"); return; }
|
||||
|
||||
double t_end = now() + seconds;
|
||||
double prev = now();
|
||||
size_t n = 0;
|
||||
while (n < max_samples) {
|
||||
double t = now();
|
||||
double d = (t - prev) * 1e6; /* microseconds */
|
||||
prev = t;
|
||||
gaps[n++] = d;
|
||||
if (t > t_end) break;
|
||||
}
|
||||
|
||||
qsort(gaps, n, sizeof(double), cmp_double);
|
||||
printf("jitter cpu=%d samples=%zu floor_us=%.3f p50=%.3f p99=%.3f p99.9=%.3f p99.99=%.3f max_us=%.1f\n",
|
||||
cpu, n, gaps[0], pct(gaps, n, 0.50), pct(gaps, n, 0.99),
|
||||
pct(gaps, n, 0.999), pct(gaps, n, 0.9999), gaps[n - 1]);
|
||||
|
||||
/* count of stalls over thresholds - these are the visible hitches */
|
||||
size_t o10 = 0, o100 = 0, o1000 = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (gaps[i] > 10.0) o10++;
|
||||
if (gaps[i] > 100.0) o100++;
|
||||
if (gaps[i] > 1000.0) o1000++;
|
||||
}
|
||||
printf("jitter cpu=%d stalls_over_10us=%zu over_100us=%zu over_1ms=%zu\n",
|
||||
cpu, o10, o100, o1000);
|
||||
free(gaps);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- diskio ---- */
|
||||
/*
|
||||
* Unbuffered file IO, so the guest's page cache is out of the way and what gets
|
||||
* measured is the virtual disk path: guest driver -> QEMU device model ->
|
||||
* emulator/IO thread -> host file. 4K random reads are the number that matters
|
||||
* for scattered small-block access; sequential is the easy case.
|
||||
*
|
||||
* FILE_FLAG_NO_BUFFERING requires sector-aligned buffers, offsets and lengths.
|
||||
*/
|
||||
#define IO_FILE "C:\\Users\\User\\vmbench_io.tmp"
|
||||
#define IO_SIZE (512u << 20) /* 512 MiB */
|
||||
#define IO_BLK 4096
|
||||
|
||||
static void diskio(void)
|
||||
{
|
||||
HANDLE h = CreateFileA(IO_FILE, GENERIC_READ | GENERIC_WRITE,
|
||||
0, NULL, CREATE_ALWAYS,
|
||||
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH, NULL);
|
||||
if (h == INVALID_HANDLE_VALUE) { printf("diskio open failed %lu\n", GetLastError()); return; }
|
||||
|
||||
void *buf = _aligned_malloc(1u << 20, 4096);
|
||||
memset(buf, 0xA5, 1u << 20);
|
||||
|
||||
/* lay the file down, and measure sequential write in the same pass */
|
||||
DWORD got;
|
||||
double t0 = now();
|
||||
for (size_t off = 0; off < IO_SIZE; off += (1u << 20))
|
||||
WriteFile(h, buf, 1u << 20, &got, NULL);
|
||||
FlushFileBuffers(h);
|
||||
double t1 = now();
|
||||
printf("diskio seq_write_MBs=%.1f\n", (double)IO_SIZE / (t1 - t0) / 1e6);
|
||||
|
||||
/* sequential read */
|
||||
LARGE_INTEGER z = {0}; SetFilePointerEx(h, z, NULL, FILE_BEGIN);
|
||||
t0 = now();
|
||||
for (size_t off = 0; off < IO_SIZE; off += (1u << 20))
|
||||
ReadFile(h, buf, 1u << 20, &got, NULL);
|
||||
t1 = now();
|
||||
printf("diskio seq_read_MBs=%.1f\n", (double)IO_SIZE / (t1 - t0) / 1e6);
|
||||
|
||||
/* 4K random read - the one that shows device-model latency */
|
||||
const int N = 20000;
|
||||
size_t blocks = IO_SIZE / IO_BLK;
|
||||
t0 = now();
|
||||
for (int i = 0; i < N; i++) {
|
||||
LARGE_INTEGER p;
|
||||
p.QuadPart = (LONGLONG)((rng() % blocks) * IO_BLK);
|
||||
SetFilePointerEx(h, p, NULL, FILE_BEGIN);
|
||||
ReadFile(h, buf, IO_BLK, &got, NULL);
|
||||
}
|
||||
t1 = now();
|
||||
double us = (t1 - t0) / N * 1e6;
|
||||
printf("diskio rand4k_IOPS=%.0f rand4k_lat_us=%.1f\n", N / (t1 - t0), us);
|
||||
|
||||
_aligned_free(buf);
|
||||
CloseHandle(h);
|
||||
DeleteFileA(IO_FILE);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- clock ---- */
|
||||
/*
|
||||
* How expensive is asking the time? Software that polls the clock in a tight
|
||||
* loop calls QPC thousands of times a second, so if the guest's clock source
|
||||
* traps to the hypervisor it shows up as a flat tax on everything and as a
|
||||
* longer scheduling tail.
|
||||
*
|
||||
* Bare metal Windows serves QueryPerformanceCounter from the TSC in tens of ns.
|
||||
* If this reports hundreds of ns or microseconds, Windows has fallen back to a
|
||||
* clock that costs a VMEXIT per read.
|
||||
*/
|
||||
static void bench_clock(void)
|
||||
{
|
||||
pin(0);
|
||||
const uint64_t N = 2000000;
|
||||
LARGE_INTEGER c;
|
||||
volatile uint64_t acc = 0;
|
||||
|
||||
for (uint64_t i = 0; i < 10000; i++) { QueryPerformanceCounter(&c); acc += c.QuadPart; }
|
||||
|
||||
double t0 = now();
|
||||
for (uint64_t i = 0; i < N; i++) { QueryPerformanceCounter(&c); acc += (uint64_t)c.QuadPart; }
|
||||
double t1 = now();
|
||||
double qpc_ns = (t1 - t0) / (double)N * 1e9;
|
||||
|
||||
t0 = now();
|
||||
for (uint64_t i = 0; i < N; i++) acc += __rdtsc();
|
||||
t1 = now();
|
||||
double tsc_ns = (t1 - t0) / (double)N * 1e9;
|
||||
|
||||
t0 = now();
|
||||
for (uint64_t i = 0; i < N; i++) acc += GetTickCount64();
|
||||
t1 = now();
|
||||
double gtc_ns = (t1 - t0) / (double)N * 1e9;
|
||||
|
||||
sink_u64 = acc;
|
||||
printf("clock qpc_ns=%.1f rdtsc_ns=%.1f gettickcount_ns=%.1f qpc_freq=%.0f\n",
|
||||
qpc_ns, tsc_ns, gtc_ns, qpc_freq);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- main ----- */
|
||||
static void usage(void)
|
||||
{
|
||||
printf("usage: vmbench <diskio|clock|cpu|cpu_mt N|memlat|membw|c2c A B|jitter CPU SECS|all>\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
LARGE_INTEGER f;
|
||||
QueryPerformanceFrequency(&f);
|
||||
qpc_freq = (double)f.QuadPart;
|
||||
|
||||
SYSTEM_INFO si;
|
||||
GetSystemInfo(&si);
|
||||
printf("# vmbench qpc_freq=%.0f logical_cpus=%lu groups=%u\n",
|
||||
qpc_freq, (unsigned long)si.dwNumberOfProcessors,
|
||||
(unsigned)GetActiveProcessorGroupCount());
|
||||
|
||||
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
|
||||
|
||||
if (argc < 2) { usage(); return 1; }
|
||||
const char *cmd = argv[1];
|
||||
int all = strcmp(cmd, "all") == 0;
|
||||
|
||||
if (all || !strcmp(cmd, "clock")) {
|
||||
bench_clock();
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "diskio")) {
|
||||
diskio();
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "cpu")) {
|
||||
pin(0);
|
||||
double best_l = 0, best_t = 0, best_f = 0;
|
||||
for (int r = 0; r < 5; r++) {
|
||||
double l = cpu_int_latency(200000000ull);
|
||||
double t = cpu_int_throughput(100000000ull);
|
||||
double fp = cpu_fp(100000000ull);
|
||||
if (l > best_l) best_l = l;
|
||||
if (t > best_t) best_t = t;
|
||||
if (fp > best_f) best_f = fp;
|
||||
}
|
||||
printf("cpu int_latency_Mops=%.1f int_throughput_Mops=%.1f fp_Mops=%.1f\n",
|
||||
best_l, best_t, best_f);
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "cpu_mt")) {
|
||||
int n = (!all && argc > 2) ? atoi(argv[2]) : (int)si.dwNumberOfProcessors;
|
||||
bench_cpu_mt(n, 100000000ull);
|
||||
if (all) { bench_cpu_mt(1, 100000000ull); bench_cpu_mt(8, 100000000ull); }
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "memlat")) {
|
||||
pin(0);
|
||||
size_t sizes[] = { 32u<<10, 512u<<10, 8u<<20, 64u<<20, 256u<<20 };
|
||||
const char *names[] = { "L1_32K", "L2_512K", "L3_8M", "DRAM_64M", "DRAM_256M" };
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double best = 1e9;
|
||||
for (int r = 0; r < 3; r++) {
|
||||
double v = memlat(sizes[i], 20000000ull);
|
||||
if (v > 0 && v < best) best = v;
|
||||
}
|
||||
printf("memlat %s ns=%.2f\n", names[i], best);
|
||||
}
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "membw")) {
|
||||
pin(0);
|
||||
double br = 0, bc = 0;
|
||||
for (int r = 0; r < 3; r++) {
|
||||
double x = membw_read(256u<<20); if (x > br) br = x;
|
||||
double y = membw_copy(256u<<20); if (y > bc) bc = y;
|
||||
}
|
||||
printf("membw read_GBs=%.2f copy_GBs=%.2f\n", br, bc);
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "c2c")) {
|
||||
if (!all && argc > 3) {
|
||||
printf("c2c %s->%s ns=%.1f\n", argv[2], argv[3],
|
||||
c2c(atoi(argv[2]), atoi(argv[3]), 200000));
|
||||
} else {
|
||||
int nc = (int)si.dwNumberOfProcessors;
|
||||
printf("c2c 0->1_ns=%.1f\n", c2c(0, 1, 200000));
|
||||
if (nc > 8) printf("c2c 0->8_ns=%.1f\n", c2c(0, 8, 200000));
|
||||
if (nc > 16) printf("c2c 0->16_ns=%.1f\n", c2c(0, 16, 200000));
|
||||
if (nc > 24) printf("c2c 0->24_ns=%.1f\n", c2c(0, 24, 200000));
|
||||
}
|
||||
}
|
||||
|
||||
if (all || !strcmp(cmd, "jitter")) {
|
||||
int cpu = (!all && argc > 2) ? atoi(argv[2]) : 0;
|
||||
double se = (!all && argc > 3) ? atof(argv[3]) : 10.0;
|
||||
jitter(cpu, se, 20000000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user