From: David Woodhouse <[email protected]> Measure L1<->L2 transition costs from L1's viewpoint, in TSC cycles: a reflected-exit phase (cpuid in L2, forwarded to L1: the full emulated vmexit->vmentry round trip, exercising the vmcb12/vmcs12 access paths), and on SVM an MSR-intercept phase (rdmsr in L2 with INTERCEPT_MSR_PROT: exercises the per-intercept lookup in L1's MSR permissions map). Reports min/avg/p50/p99/max over 100k iterations.
Works on both VMX and SVM. Signed-off-by: David Woodhouse <[email protected]> Assisted-by: Claude:claude-mythos-5 --- tools/testing/selftests/kvm/Makefile.kvm | 1 + .../kvm/x86/nested_transition_bench.c | 204 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 tools/testing/selftests/kvm/x86/nested_transition_bench.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index b30fa994d5e5..2c5cd619f747 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -156,6 +156,7 @@ TEST_GEN_PROGS_x86 += x86/triple_fault_event_test TEST_GEN_PROGS_x86 += x86/recalc_apic_map_test TEST_GEN_PROGS_x86 += x86/aperfmperf_test TEST_GEN_PROGS_x86 += x86/vmx_apic_update_test +TEST_GEN_PROGS_x86 += x86/nested_transition_bench TEST_GEN_PROGS_x86 += access_tracking_perf_test TEST_GEN_PROGS_x86 += coalesced_io_test TEST_GEN_PROGS_x86 += dirty_log_perf_test diff --git a/tools/testing/selftests/kvm/x86/nested_transition_bench.c b/tools/testing/selftests/kvm/x86/nested_transition_bench.c new file mode 100644 index 000000000000..12fc1ac610e9 --- /dev/null +++ b/tools/testing/selftests/kvm/x86/nested_transition_bench.c @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * nested_transition_bench + * + * Measure nested virtualization transition costs: + * 1. L2 exit reflected to L1 (cpuid): full emulated vmexit->vmentry + * round trip, exercising the vmcb12/vmcs12 access paths. + * 2. L2 MSR access with MSR intercepts enabled: exercises the + * per-intercept L1 permissions-bitmap lookup on SVM, and the + * bitmap merge path when force_msr_bitmap_recalc is set. + * + * Reports TSC cycles per iteration (min/avg/p99) from L1's viewpoint. + */ +#include <stdio.h> +#include <stdlib.h> + +#include "test_util.h" +#include "kvm_util.h" +#include "processor.h" +#include "vmx.h" +#include "svm_util.h" + +/* + * Keep the iteration count modest: every timed iteration includes a + * ucall round trip to userspace to report its sample, which on a + * debug (KASAN) kernel costs milliseconds. 10k samples still give a + * stable p99. + */ +#define N_WARMUP 1000 +#define N_ITERS 10000 + + +static uint64_t results[N_ITERS]; + +/* L2: exit to L1 in a tight loop. */ +static void l2_guest_code(void) +{ + u32 eax, ebx, ecx, edx; + + for (;;) + cpuid(0, &eax, &ebx, &ecx, &edx); +} + +static void l2_guest_code_msr(void) +{ + for (;;) + rdmsr(MSR_IA32_TSC_DEADLINE); +} + +/* SVM L1: run L2, timing VMRUN -> #VMEXIT round trips. */ +static void l1_svm_code(struct svm_test_data *svm) +{ + struct vmcb *vmcb = svm->vmcb; + uint64_t t0, t1; + int i; + + generic_svm_setup(svm, l2_guest_code); + /* + * Unlike VMX, where CPUID unconditionally exits, SVM only + * intercepts CPUID if asked; without it L0 emulates L2's cpuid + * and resumes L2 directly, and run_guest() never returns. + */ + vmcb->control.intercept |= BIT_ULL(INTERCEPT_CPUID) | + BIT_ULL(INTERCEPT_MSR_PROT); + + /* + * Set the read-intercept bit for MSR_IA32_TSC_DEADLINE (0x6e0, + * MSRPM range 0: bit 2*msr, i.e. byte 0x1b8 bit 0) in L1's MSRPM, + * so that L2's rdmsr in phase 2 reflects to L1. L0's per-exit + * lookup in this bitmap is the path being measured. + */ + ((u8 *)svm->msr)[(MSR_IA32_TSC_DEADLINE & 0x1fff) * 2 / 8] |= + BIT((MSR_IA32_TSC_DEADLINE & 0x1fff) * 2 % 8); + + for (i = 0; i < N_WARMUP; i++) { + run_guest(vmcb, svm->vmcb_gpa); + GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_CPUID); + vmcb->save.rip += 2; /* cpuid */ + } + + for (i = 0; i < N_ITERS; i++) { + t0 = rdtsc(); + run_guest(vmcb, svm->vmcb_gpa); + t1 = rdtsc(); + GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_CPUID); + vmcb->save.rip += 2; + GUEST_SYNC1(t1 - t0); + } + + /* Phase 2: MSR intercept path */ + vmcb->save.rip = (u64)l2_guest_code_msr; + for (i = 0; i < N_ITERS; i++) { + t0 = rdtsc(); + run_guest(vmcb, svm->vmcb_gpa); + t1 = rdtsc(); + GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_MSR); + vmcb->save.rip += 2; /* rdmsr */ + GUEST_SYNC1(t1 - t0); + } + + GUEST_DONE(); +} + +/* VMX L1: run L2, timing VMRESUME -> vmexit round trips. */ +static void l1_vmx_code(struct vmx_pages *vmx) +{ + uint64_t t0, t1; + int i; + + GUEST_ASSERT(prepare_for_vmx_operation(vmx)); + GUEST_ASSERT(load_vmcs(vmx)); + prepare_vmcs(vmx, l2_guest_code); + + GUEST_ASSERT(!vmlaunch()); + /* first exit is here; loop resumes */ + for (i = 0; i < N_WARMUP; i++) { + GUEST_ASSERT(vmreadz(VM_EXIT_REASON) == EXIT_REASON_CPUID); + vmwrite(GUEST_RIP, vmreadz(GUEST_RIP) + + vmreadz(VM_EXIT_INSTRUCTION_LEN)); + GUEST_ASSERT(!vmresume()); + } + + for (i = 0; i < N_ITERS; i++) { + GUEST_ASSERT(vmreadz(VM_EXIT_REASON) == EXIT_REASON_CPUID); + vmwrite(GUEST_RIP, vmreadz(GUEST_RIP) + + vmreadz(VM_EXIT_INSTRUCTION_LEN)); + t0 = rdtsc(); + GUEST_ASSERT(!vmresume()); + t1 = rdtsc(); + GUEST_SYNC1(t1 - t0); + } + + GUEST_DONE(); +} + +static int cmp_u64(const void *a, const void *b) +{ + uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b; + + return x < y ? -1 : x > y; +} + +static void report(const char *phase, int n) +{ + uint64_t sum = 0; + int i; + + qsort(results, n, sizeof(results[0]), cmp_u64); + for (i = 0; i < n; i++) + sum += results[i]; + + printf("%-24s n=%d min=%" PRIu64 " avg=%" PRIu64 " p50=%" PRIu64 + " p99=%" PRIu64 " max=%" PRIu64 " cycles\n", + phase, n, results[0], sum / n, results[n / 2], + results[(int)(n * 0.99)], results[n - 1]); +} + +int main(int argc, char *argv[]) +{ + struct kvm_vcpu *vcpu; + struct kvm_vm *vm; + struct ucall uc; + gva_t nested_gva = 0; + bool is_svm = kvm_cpu_has(X86_FEATURE_SVM); + int n = 0, phase = 0; + bool done = false; + + TEST_REQUIRE(is_svm || kvm_cpu_has(X86_FEATURE_VMX)); + + vm = vm_create_with_one_vcpu(&vcpu, is_svm ? (void *)l1_svm_code + : (void *)l1_vmx_code); + if (is_svm) + vcpu_alloc_svm(vm, &nested_gva); + else + vcpu_alloc_vmx(vm, &nested_gva); + vcpu_args_set(vcpu, 1, nested_gva); + + while (!done) { + vcpu_run(vcpu); + switch (get_ucall(vcpu, &uc)) { + case UCALL_SYNC: + results[n++] = uc.args[0]; + if (n == N_ITERS) { + report(phase == 0 ? "reflected-exit" : + "msr-intercept", n); + n = 0; + phase++; + } + break; + case UCALL_ABORT: + REPORT_GUEST_ASSERT(uc); + case UCALL_DONE: + if (n) + report("partial", n); + done = true; + break; + default: + TEST_FAIL("Unknown ucall %lu", uc.cmd); + } + } + + kvm_vm_free(vm); + return 0; +} -- 2.55.0

