Skip to content

Chi phí giả lập của QEMU: KVM và TCG

Board embedded thường dùng ARM, nên phần mềm của board thường được thử trên máy dev x86 bằng giả lập. Cách hiểu thường gặp là phần chậm nằm ở việc khác kiến trúc. Số đo trên một máy dev cho kết quả ngược lại: guest x86-64 chạy bằng TCG trên host x86-64, tức cùng kiến trúc, đã chậm hơn KVM khoảng 7 lần với cùng một tải CPU, còn chuyển sang guest aarch64 chỉ chậm thêm khoảng 1,3 lần. Phần lớn chi phí nằm ở bản thân việc giả lập.

Trang trình bày lần lượt cơ chế của hai chế độ chạy, cách thiết kế phép đo, kết quả, giới hạn của phép đo, cách chọn chế độ theo nhu cầu, và cuối cùng là các bước tái hiện kết quả (chuẩn bị máy, bật nested VT-x, boot guest).

Hai chế độ chạy của QEMU

QEMU chạy mã của guest theo một trong hai cách:

Chế độ Cơ chế Kiến trúc guest Chi phí chính
KVM (-accel kvm) Lệnh của guest chạy trực tiếp trên CPU nhờ phần mở rộng ảo hóa (VT-x, AMD-V) Phải trùng với host VM exit: I/O, ngắt, một số lệnh đặc quyền
TCG (-accel tcg) QEMU dịch từng khối lệnh của guest sang lệnh của host rồi chạy mã đã dịch (giả lập) Bất kỳ kiến trúc QEMU hỗ trợ Dịch lệnh, tra bảng dịch địa chỉ ở mỗi truy cập bộ nhớ

With KVM, guest instructions run directly on the host CPU and only VM exits return to KVM and QEMU; with TCG, the main loop looks up or translates each guest block, runs the translated host code, and every load and store adds a software TLB lookup

Đường chạy lệnh với KVM

Với KVM, CPU chạy guest ở chế độ dành cho guest (VMX non-root trên Intel). Lệnh thông thường của guest, kể cả lệnh truy cập bộ nhớ, chạy thẳng trên CPU mà không qua phần mềm nào. Chỉ những sự kiện cần hypervisor xử lý, như truy cập thiết bị ảo hay ngắt, mới gây VM exit để chuyển quyền điều khiển về KVM và QEMU. Chi tiết về VM exit nằm ở Intel VT-x, cách QEMU và KVM chia việc nằm ở KVM & QEMU.

Đường chạy lệnh với TCG

TCG (Tiny Code Generator) là bộ dịch động (dynamic translator) của QEMU. Lần đầu gặp một đoạn mã của guest, QEMU dịch đoạn đó sang tập lệnh của host. Đơn vị dịch là translation block (TB), là một khối lệnh liên tiếp, thường kết thúc ở lệnh rẽ nhánh. Lệnh guest được chuyển sang dạng trung gian gọi là TCG ops, rồi từ đó sinh mã host. TB đã dịch được giữ lại để dùng lại. Mỗi TB còn ghi kèm trạng thái CPU mà bộ dịch giả định là không đổi, ví dụ mức đặc quyền. Khi trạng thái đó thay đổi, QEMU phải dùng hoặc tạo một TB khác.

Khi chạy xong một TB, QEMU cần tìm TB kế tiếp theo PC của guest. Cách đơn giản nhất là thoát khỏi mã đã dịch, quay về vòng lặp chính (main loop) để tra cứu, và dịch mới nếu chưa có. Để tránh vòng quay này, QEMU vá lệnh nhảy ở cuối TB để nhảy thẳng sang TB đích (direct block chaining). Cơ chế này chỉ áp dụng với nhánh có đích cố định và đích nằm cùng trang bộ nhớ. Theo tài liệu thiết kế của QEMU, tra cứu TB kế tiếp là lý do thường gặp nhất khiến mã đã dịch phải thoát ra main loop.

Bên trong mã đã dịch còn có thêm việc mà KVM không phải làm:

  • Truy cập bộ nhớ. Ở chế độ giả lập cả hệ thống, QEMU dùng MMU phần mềm (softmmu): mỗi lệnh truy cập bộ nhớ của guest đều phải dịch địa chỉ ảo sang địa chỉ vật lý. QEMU có một TLB phần mềm để tăng tốc bước này. Truy cập RAM nhanh hơn vì TLB lưu sẵn offset tới bộ nhớ của host, còn truy cập vùng MMIO phải gọi mã C giả lập thiết bị.
  • Lệnh phức tạp. Lệnh phức tạp hoặc ít dùng thường được cài đặt bằng lời gọi tới một hàm C (helper) thay vì sinh mã trực tiếp.
  • Mã tự sửa (self-modifying code). Khi guest ghi vào trang bộ nhớ chứa mã đã được dịch, QEMU phải hủy các TB thuộc trang đó. Ở chế độ giả lập cả hệ thống, việc phát hiện ghi cũng đi qua MMU phần mềm.

Không bước nào ở trên phụ thuộc vào việc guest khác kiến trúc host. Guest x86-64 chạy bằng TCG trên host x86-64 vẫn đi qua đủ các bước dịch, tra TB và tra TLB phần mềm. Phép đo ở trang này không tách được chi phí của từng bước, nhưng cho biết tổng chi phí của cả cơ chế so với KVM.

Thiết kế phép đo

Phép đo dùng ba cấu hình, ghép thành hai cặp so sánh. Trong mỗi cặp, chỉ một nhóm yếu tố thay đổi:

Cặp so sánh Yếu tố khác nhau Chi phí đo được
x86-64 + KVM và x86-64 + TCG Chỉ bộ tăng tốc (-accel, -cpu) Chi phí của giả lập
x86-64 + TCG và aarch64 + TCG Kiến trúc, bản build kernel, loại máy (q35 so với virt), model CPU Ước lượng chi phí của việc khác kiến trúc

Cấu hình x86-64 + TCG là cấu hình đối chứng. Nếu chỉ so guest x86-64 dùng KVM với guest aarch64 dùng TCG, hai chi phí gộp lại thành một con số khoảng 9 lần và không biết phần nào đến từ giả lập, phần nào đến từ khác kiến trúc.

Mỗi lần đo boot guest với busybox shell làm init, gửi một tải CPU cố định qua serial và đo bằng đồng hồ host, vì đồng hồ trong guest không đáng tin khi chạy TCG:

for cfg in [x86-64 + KVM, x86-64 + TCG, aarch64 + TCG]:
    repeat until 5 valid runs:
        start QEMU with cfg, rdinit=/bin/sh, serial on stdio
        wait for shell prompt "~ # "
        send: busybox --install -s; echo START;
              dd if=/dev/zero bs=1M count=200 | gzip -1 | sha256sum;
              echo END; poweroff -f
        workload time = host time at "END" - host time at "START"
        drop the run if the kernel panics
    report the median per cfg

Cả ba cấu hình cho cùng một hash sha256sum, nên tải giống hệt nhau. Script đầy đủ (bench.py) và số liệu từng lần chạy nằm trong repo lab.

Môi trường đo: host vật lý chạy Ubuntu 20.04.6 (kernel 5.15) trên CPU Intel Core i9-7980XE, với VirtualBox 7.1.12. VM dev chạy Ubuntu 24.04.5 (kernel 6.8), 8 vCPU, khoảng 12 GB RAM, QEMU 8.2.2. Mỗi guest có -smp 2 -m 1G.

Máy dev trong phép đo là một VM, nên có hai tầng ảo hóa. Máy vật lý (L0) chạy VirtualBox, VM dev (L1) chạy QEMU, và guest của QEMU là L2. Guest x86-64 chỉ dùng được KVM nếu L0 bật nested virtualization, tức cho L1 thấy các lệnh ảo hóa phần cứng. Hai cấu hình TCG không dùng VT-x nên không phụ thuộc vào nested virtualization.

Three measured configurations on a dev VM inside VirtualBox with nested VT-x: an x86-64 guest with KVM, the same x86-64 guest with TCG as the control, and an aarch64 guest with TCG

Kết quả

Bar chart of the median CPU workload time: 1.52 s with KVM, 11.00 s with x86-64 TCG and 14.04 s with aarch64 TCG, with the 9.48 s step from KVM to TCG marked as translation cost and the 3.04 s step to aarch64 marked as foreign ISA, not isolated

Cấu hình Tải CPU, 5 lần (s) Trung vị (s) So với KVM
x86-64 + KVM 1,51 / 1,54 / 1,57 / 1,51 / 1,52 1,52 1×
x86-64 + TCG 11,00 / 11,46 / 10,67 / 10,53 / 11,03 11,00 7,2×
aarch64 + TCG 14,36 / 12,64 / 14,04 / 14,33 / 12,86 14,04 9,2×

Phần lớn chi phí nằm ở giả lập. Chỉ đổi bộ tăng tốc từ KVM sang TCG, giữ nguyên kiến trúc, kernel và loại máy, thời gian chạy tải tăng từ 1,52 s lên 11,00 s, tức 7,2 lần. Chuyển tiếp sang guest aarch64 chỉ tăng thêm 3,04 s, tức 1,28 lần. Trong 12,52 s chênh lệch giữa KVM và aarch64 TCG, khoảng ba phần tư (9,48 s) xuất hiện ngay khi chuyển sang TCG, trước khi kiến trúc thay đổi.

Kết quả này khớp với cơ chế ở trên: TCG luôn dịch lệnh và luôn tra TLB phần mềm ở mỗi truy cập bộ nhớ, dù guest cùng hay khác kiến trúc với host. Khác kiến trúc chỉ làm thay đổi nội dung bản dịch, không thêm một bước mới.

Phần chênh 1,28 lần của cặp thứ hai nhiều khả năng chủ yếu do khác kiến trúc, nhưng chưa tách riêng được, vì cặp này còn khác bản build kernel, loại máy và model CPU. Về độ ổn định, KVM gần như không dao động (1,51 đến 1,57 s), còn hai cấu hình TCG dao động khoảng 1 s giữa các lần chạy.

Thời gian boot đến shell

Thời gian boot cho thứ tự khác với tải CPU: guest aarch64 lên shell nhanh hơn guest x86-64 chạy TCG. Nhiều khả năng do guest x86 đi qua SeaBIOS trước khi vào kernel, còn guest ARM với -kernel boot thẳng vào kernel, và cấu hình kernel của hai kiến trúc cũng khác nhau. Giả thuyết này chưa được kiểm chứng.

Cấu hình Boot đến shell, trung vị 5 lần (s)
x86-64 + KVM 1,74
x86-64 + TCG 5,76
aarch64 + TCG 3,98

Giới hạn của phép đo

  • Hai tầng ảo hóa. Mỗi VM exit của guest dùng KVM đi qua cả KVM trong VM dev và VirtualBox ở host. Các số đo chỉ so sánh tương đối trên máy này, không đại diện cho máy thật.
  • Không có mốc native. Phép đo không chạy cùng tải trực tiếp trên VM dev, nên chưa biết guest dùng KVM cách tốc độ native bao xa trong cấu hình nested.
  • Cặp so sánh kiến trúc chưa tách riêng. Như đã nêu ở phần kết quả, con số 1,28 lần còn gộp khác biệt về kernel, loại máy và model CPU.
  • Kernel panic khi boot x86-64 bằng TCG. 3 trên 12 lần boot x86-64 TCG bị panic sớm với thông báo IO-APIC + timer doesn't work!, còn KVM và aarch64 không lần nào gặp. TCG không dùng VT-x, nên lỗi này không liên quan đến nested virtualization. Nhiều khả năng giả lập chậm làm bước kiểm tra timer của kernel thất bại (chưa kiểm chứng). Các lần panic bị loại khỏi kết quả.

Todo

  • Chạy cùng tải trực tiếp trên VM dev để có mốc native, và đo chi phí của nested virtualization.
  • Chạy cùng tải bằng QEMU user-mode aarch64 (busybox static). User-mode chỉ chạy chương trình, không giả lập MMU của guest, nên phép đo này tách được chi phí dịch lệnh khỏi chi phí giả lập cả hệ thống.
  • Thử tham số kernel no_timer_check, cách thường được dùng để bỏ qua bước kiểm tra timer gây panic ở trên.

Chọn chế độ theo nhu cầu

Nhu cầu Cách chạy Chi phí trên máy đo
Chạy guest x86-64: thử kernel, driver, kernel nhiều instance như kmorph KVM 1× (mốc so sánh)
Thử kernel và rootfs cho board ARM trước khi có board qemu-system-aarch64 với TCG khoảng 9×
Chạy một chương trình hoặc container ARM trên máy x86 QEMU user-mode chưa đo
Debug kernel bằng gdb (-s -S) KVM nếu cùng kiến trúc, TCG nếu khác, xem KVM & QEMU như hai dòng đầu
Kiểm tra hành vi thiết bị thật: NIC, FPGA, card tăng tốc khi reset hoặc khi còn DMA Board thật không giả lập được: guest chỉ thấy thiết bị ảo (virtio, IOMMU ảo)

Vì phần lớn chi phí nằm ở giả lập chứ không ở khác kiến trúc, nên dùng KVM mỗi khi kiến trúc guest trùng host. Chạy guest cùng kiến trúc bằng TCG chỉ có ích khi máy không có KVM, ví dụ VM dev chưa bật nested VT-x. Với guest ARM trên máy x86, TCG là lựa chọn duy nhất của QEMU, và chi phí phải chấp nhận phần lớn là chi phí giả lập, không phải chi phí của ARM.

Tái hiện kết quả

Chuẩn bị máy

Cài QEMU cho cả hai kiến trúc:

sudo apt install qemu-system-x86 qemu-system-arm

Kiểm tra máy dùng được KVM. Nếu máy dev là VM, cần bật nested VT-x trước, xem mục con bên dưới.

grep -Ec '(vmx|svm)' /proc/cpuinfo    # > 0: CPU có cờ ảo hóa
ls -l /dev/kvm                        # có node này: KVM sẵn sàng
id | grep -o kvm                      # người dùng thuộc nhóm kvm

Lệnh grep -Ec đếm số dòng khớp, không phải số CPU: trên máy 8 vCPU kết quả là 16, vì mỗi CPU có cả dòng flags và dòng vmx flags. Người dùng cần thuộc nhóm kvm để mở /dev/kvm, và phải đăng nhập lại sau khi được thêm vào nhóm.

Xác nhận QEMU dùng được KVM bằng cách khởi tạo một VM trống, dừng ngay ở -S và hỏi qua monitor:

printf 'info kvm\nquit\n' | qemu-system-x86_64 -accel kvm -M q35 -cpu host \
    -display none -S -monitor stdio

Kết quả mong đợi có dòng kvm support: enabled.

Nested VT-x khi máy dev là VM

Nếu máy dev là VM và không có /dev/kvm, cần bật nested virtualization ở phía host khi VM đang tắt hẳn. Với VirtualBox:

  • Giao diện: Settings → System → Processor → Enable Nested VT-x/AMD-V.
  • Dòng lệnh trên host: VBoxManage modifyvm "<tên VM>" --nested-hw-virt on.

Điều kiện:

  • CPU host có VT-x hoặc AMD-V và đã bật trong firmware.
  • VirtualBox hỗ trợ nested virtualization từ bản 6.0.0 với CPU AMD, và từ bản 6.1.0 với CPU Intel từ thế hệ Core i thứ 5 (Broadwell) trở lên.
  • Trên host Windows, nếu Hyper-V hoặc các tính năng dựa trên nó (WSL2, Memory Integrity) đang chạy, VirtualBox có thể dùng Hyper-V làm backend và nested VT-x không hoạt động như mong đợi.

Kiểm tra từ phía host:

VBoxManage showvminfo "<tên VM>" | grep -iE "nested|paravirt"
Nested VT-x/AMD-V:           enabled
Nested Paging:               enabled

Sau khi khởi động lại VM, các lệnh kiểm tra ở trên phải cho kết quả dương, và /sys/module/kvm_intel/parameters/nested có giá trị Y. Lệnh systemd-detect-virt vẫn trả về oracle, vì VM vẫn chạy trên VirtualBox, nên không dùng lệnh này để kiểm tra nested. Khi mọi thứ đã chạy, nên tạo một snapshot của VM để quay lại khi thử nghiệm làm hỏng môi trường.

Nested virtualization trên VirtualBox kém ổn định hơn KVM trên bare metal. Lỗi lạ khi guest dùng NMI hoặc INIT giữa các vCPU cần được đối chiếu với một lần chạy trên máy thật trước khi kết luận.

Kernel và initramfs

Guest dùng bản netboot của Alpine 3.24.4, flavor virt (kernel 6.18.60), có cho cả x86_64 và aarch64. Mỗi bản gồm một kernel và một initramfs có busybox, đủ để boot đến shell mà không cần disk hay mạng:

V=3.24.4
for a in x86_64 aarch64; do
    curl -fLO https://dl-cdn.alpinelinux.org/alpine/v3.24/releases/$a/alpine-netboot-$V-$a.tar.gz
    mkdir -p $a && tar -xzf alpine-netboot-$V-$a.tar.gz -C $a boot/vmlinuz-virt boot/initramfs-virt
done

Script fetch-alpine.sh trong repo lab làm việc này và kiểm tra sha256 của từng file.

Lệnh boot

Guest x86-64 với KVM, dùng busybox shell làm init (rdinit=/bin/sh), console trên cổng serial:

qemu-system-x86_64 -accel kvm -cpu host -M q35 -smp 2 -m 1G -nographic \
    -kernel x86_64/boot/vmlinuz-virt -initrd x86_64/boot/initramfs-virt \
    -append "console=ttyS0 rdinit=/bin/sh"

Cấu hình đối chứng chạy cùng guest bằng TCG: thay -accel kvm -cpu host bằng -accel tcg -cpu max.

Guest aarch64, luôn chạy bằng TCG, trên máy virt của QEMU:

qemu-system-aarch64 -accel tcg -cpu max,pauth-impdef=on -M virt -smp 2 -m 1G -nographic \
    -kernel aarch64/boot/vmlinuz-virt -initrd aarch64/boot/initramfs-virt \
    -append "console=ttyAMA0 rdinit=/bin/sh"

Tùy chọn pauth-impdef=on chọn thuật toán pointer authentication do QEMU tự định nghĩa thay cho thuật toán chuẩn QARMA5 của Arm. Thuật toán của QEMU không có tính chất mật mã nhưng giả lập nhanh hơn đáng kể.

Khi boot xong, serial hiện dòng /bin/sh: can't access tty; job control turned off và prompt ~ #. Initramfs của Alpine chỉ tạo sẵn vài link cho busybox, nên cần chạy /bin/busybox --install -s trước khi dùng các lệnh như gzip hay poweroff. Thoát QEMU bằng Ctrl+A rồi X.

Khi boot guest x86-64 bằng TCG, thỉnh thoảng kernel bị panic sớm với thông báo IO-APIC + timer doesn't work!. Chạy lại lệnh là đủ, chi tiết ở mục Giới hạn của phép đo. Script bench.py trong repo lab chạy toàn bộ phép đo ở trên và tự loại các lần panic.

Liên quan

Tài liệu tham khảo