Skip to content

Commit 75b6554

Browse files
committed
Reduce timer checking overhead with lazy evaluation
This reduces CPU overhead from timer interrupt checking by implementing: 1. Lazy timer checking: Cache next_interrupt_at (earliest interrupt time across all harts) to skip expensive checks when no interrupt is due. 2. Event-driven wait: Replace fixed 1ms periodic timer with dynamic one-shot timers (kqueue on macOS, timerfd on Linux) that wake exactly when next interrupt is due.
1 parent bb10925 commit 75b6554

File tree

3 files changed

+146
-43
lines changed

3 files changed

+146
-43
lines changed

aclint.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@
44
#include "riscv_private.h"
55

66
/* ACLINT MTIMER */
7+
8+
/* Recalculate the next interrupt time by finding the minimum mtimecmp
9+
* across all harts. This is called whenever mtimecmp is written.
10+
*/
11+
void aclint_mtimer_recalc_next_interrupt(mtimer_state_t *mtimer)
12+
{
13+
uint64_t min_cmp = UINT64_MAX;
14+
for (uint32_t i = 0; i < mtimer->n_harts; i++) {
15+
if (mtimer->mtimecmp[i] < min_cmp)
16+
min_cmp = mtimer->mtimecmp[i];
17+
}
18+
mtimer->next_interrupt_at = min_cmp;
19+
}
20+
721
void aclint_mtimer_update_interrupts(hart_t *hart, mtimer_state_t *mtimer)
822
{
923
if (semu_timer_get(&mtimer->mtime) >= mtimer->mtimecmp[hart->mhartid])
@@ -63,6 +77,11 @@ static bool aclint_mtimer_reg_write(mtimer_state_t *mtimer,
6377
cmp_val = (cmp_val & 0xFFFFFFFF00000000ULL) | value;
6478

6579
mtimer->mtimecmp[addr >> 3] = cmp_val;
80+
81+
/* Recalculate next interrupt time when mtimecmp is updated.
82+
* This is critical for lazy timer checking optimization.
83+
*/
84+
aclint_mtimer_recalc_next_interrupt(mtimer);
6685
return true;
6786
}
6887

device.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,19 @@ typedef struct {
242242
*/
243243
uint64_t *mtimecmp;
244244
semu_timer_t mtime;
245+
246+
/* Cache the earliest interrupt time to avoid checking every instruction.
247+
* Updated when:
248+
* 1. Any mtimecmp register is written (recalculate min of all harts)
249+
* 2. After an interrupt fires (kernel updates mtimecmp)
250+
* 3. Initialization (set to min of all initial mtimecmp values)
251+
*/
252+
uint64_t next_interrupt_at;
253+
uint32_t n_harts; /* Number of harts, needed for min() calculation */
245254
} mtimer_state_t;
246255

247256
void aclint_mtimer_update_interrupts(hart_t *hart, mtimer_state_t *mtimer);
257+
void aclint_mtimer_recalc_next_interrupt(mtimer_state_t *mtimer);
248258
void aclint_mtimer_read(hart_t *hart,
249259
mtimer_state_t *mtimer,
250260
uint32_t addr,

main.c

Lines changed: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,33 @@ static void emu_update_timer_interrupt(hart_t *hart)
105105
{
106106
emu_state_t *data = PRIV(hart);
107107

108-
/* Sync global timer with local timer */
108+
/* Lazy timer checking. Only check timer interrupts when the current time
109+
* has reached the earliest scheduled interrupt time. This avoids expensive
110+
* semu_timer_get() calls and interrupt checks.
111+
*
112+
* Fast path: Skip if current time < next interrupt time
113+
* Slow path: Check all harts' timers and recalculate next interrupt time
114+
*/
115+
uint64_t current_time = semu_timer_get(&data->mtimer.mtime);
116+
if (current_time < data->mtimer.next_interrupt_at) {
117+
/* Fast path: No timer interrupt can fire yet, skip checking.
118+
* Still sync the timer for correctness (hart->time is used by CSR
119+
* reads).
120+
*/
121+
hart->time = data->mtimer.mtime;
122+
return;
123+
}
124+
125+
/* Slow path: At least one timer might fire, check this hart */
109126
hart->time = data->mtimer.mtime;
110127
aclint_mtimer_update_interrupts(hart, &data->mtimer);
128+
129+
/* Recalculate next interrupt time after potential interrupt delivery.
130+
* The kernel likely updated mtimecmp in the interrupt handler, which
131+
* already called recalc, but we call it again to be safe in case
132+
* multiple harts share the same mtimecmp value.
133+
*/
134+
aclint_mtimer_recalc_next_interrupt(&data->mtimer);
111135
}
112136

113137
static void emu_update_swi_interrupt(hart_t *hart)
@@ -342,6 +366,8 @@ static inline sbi_ret_t handle_sbi_ecall_TIMER(hart_t *hart, int32_t fid)
342366
(((uint64_t) hart->x_regs[RV_R_A1]) << 32) |
343367
(uint64_t) (hart->x_regs[RV_R_A0]);
344368
hart->sip &= ~RV_INT_STI_BIT;
369+
/* Recalculate next interrupt time for lazy timer checking */
370+
aclint_mtimer_recalc_next_interrupt(&data->mtimer);
345371
return (sbi_ret_t){SBI_SUCCESS, 0};
346372
default:
347373
return (sbi_ret_t){SBI_ERR_NOT_SUPPORTED, 0};
@@ -766,6 +792,11 @@ static int semu_init(emu_state_t *emu, int argc, char **argv)
766792
/* Set up ACLINT */
767793
semu_timer_init(&emu->mtimer.mtime, CLOCK_FREQ, hart_count);
768794
emu->mtimer.mtimecmp = calloc(vm->n_hart, sizeof(uint64_t));
795+
emu->mtimer.n_harts = vm->n_hart;
796+
/* mtimecmp is initialized to 0 by calloc, so next_interrupt_at starts at 0.
797+
* It will be updated when the kernel writes mtimecmp via SBI or MMIO.
798+
*/
799+
emu->mtimer.next_interrupt_at = 0;
769800
emu->mswi.msip = calloc(vm->n_hart, sizeof(uint32_t));
770801
emu->sswi.ssip = calloc(vm->n_hart, sizeof(uint32_t));
771802
#if SEMU_HAS(VIRTIOSND)
@@ -953,6 +984,34 @@ static void print_mmu_cache_stats(vm_t *vm)
953984
}
954985
#endif
955986

987+
/* Calculate nanoseconds until next timer interrupt.
988+
* Returns 0 if interrupt is already due, or capped at 100ms maximum.
989+
*/
990+
static uint64_t calc_ns_until_next_interrupt(emu_state_t *emu)
991+
{
992+
uint64_t current_time = semu_timer_get(&emu->mtimer.mtime);
993+
uint64_t next_int = emu->mtimer.next_interrupt_at;
994+
995+
/* If interrupt is already due or very close, return immediately */
996+
if (current_time >= next_int)
997+
return 0;
998+
999+
/* Calculate ticks until interrupt */
1000+
uint64_t ticks_remaining = next_int - current_time;
1001+
1002+
/* Convert RISC-V timer ticks to nanoseconds:
1003+
* ns = ticks * (1e9 / CLOCK_FREQ)
1004+
*/
1005+
uint64_t ns = (ticks_remaining * 1000000000ULL) / emu->mtimer.mtime.freq;
1006+
1007+
/* Cap at 100ms to maintain responsiveness for UART and other events */
1008+
const uint64_t MAX_WAIT_NS = 100000000ULL; /* 100ms */
1009+
if (ns > MAX_WAIT_NS)
1010+
ns = MAX_WAIT_NS;
1011+
1012+
return ns;
1013+
}
1014+
9561015
static int semu_run(emu_state_t *emu)
9571016
{
9581017
int ret;
@@ -974,36 +1033,20 @@ static int semu_run(emu_state_t *emu)
9741033
return -1;
9751034
}
9761035

977-
/* Add 1ms periodic timer */
978-
struct kevent kev_timer;
979-
EV_SET(&kev_timer, 1, EVFILT_TIMER, EV_ADD | EV_ENABLE, 0, 1, NULL);
980-
if (kevent(kq, &kev_timer, 1, NULL, 0, NULL) < 0) {
981-
perror("kevent timer setup");
982-
close(kq);
983-
return -1;
984-
}
985-
986-
/* Note: UART input is polled via u8250_check_ready(), no need to
987-
* monitor with kqueue. Timer events are sufficient to wake from WFI.
1036+
/* Note: Timer is configured dynamically in the event loop based on
1037+
* next_interrupt_at. UART input is polled via u8250_check_ready().
9881038
*/
9891039
#else
990-
/* Linux: create timerfd for periodic wakeup */
1040+
/* Linux: create timerfd for dynamic timer wakeup */
9911041
int wfi_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
9921042
if (wfi_timer_fd < 0) {
9931043
perror("timerfd_create");
9941044
return -1;
9951045
}
9961046

997-
/* Configure 1ms periodic timer */
998-
struct itimerspec its = {
999-
.it_interval = {.tv_sec = 0, .tv_nsec = 1000000},
1000-
.it_value = {.tv_sec = 0, .tv_nsec = 1000000},
1001-
};
1002-
if (timerfd_settime(wfi_timer_fd, 0, &its, NULL) < 0) {
1003-
perror("timerfd_settime");
1004-
close(wfi_timer_fd);
1005-
return -1;
1006-
}
1047+
/* Timer is configured dynamically in the event loop based on
1048+
* next_interrupt_at to minimize unnecessary wakeups.
1049+
*/
10071050
#endif
10081051

10091052
while (!emu->stopped) {
@@ -1025,30 +1068,61 @@ static int semu_run(emu_state_t *emu)
10251068
}
10261069
if (all_waiting) {
10271070
/* All harts waiting for interrupt - use event-driven wait
1028-
* to reduce CPU usage while maintaining responsiveness
1071+
* to reduce CPU usage while maintaining responsiveness.
1072+
* Dynamically adjust timer based on next_interrupt_at.
10291073
*/
1074+
1075+
/* Calculate how long to wait until next timer interrupt */
1076+
uint64_t wait_ns = calc_ns_until_next_interrupt(emu);
1077+
1078+
/* If interrupt is already due, don't wait - continue
1079+
* immediately
1080+
*/
1081+
if (wait_ns > 0) {
10301082
#ifdef __APPLE__
1031-
/* macOS: wait for kqueue events (timer or UART) */
1032-
struct kevent events[2];
1033-
int nevents = kevent(kq, NULL, 0, events, 2, NULL);
1034-
/* Events are automatically handled - timer fires every 1ms,
1035-
* UART triggers on input. No need to explicitly consume. */
1036-
(void) nevents;
1083+
/* configure one-shot kqueue timer with dynamic timeout */
1084+
struct kevent kev_timer;
1085+
/* NOTE_USECONDS for microseconds, wait_ns/1000 converts
1086+
* ns to us
1087+
*/
1088+
EV_SET(&kev_timer, 1, EVFILT_TIMER,
1089+
EV_ADD | EV_ENABLE | EV_ONESHOT, NOTE_USECONDS,
1090+
wait_ns / 1000, NULL);
1091+
1092+
struct kevent events[2];
1093+
int nevents = kevent(kq, &kev_timer, 1, events, 2, NULL);
1094+
/* Events are automatically handled. Wakeup occurs on:
1095+
* - Timer expiration (wait_ns elapsed)
1096+
* - UART input (if monitored)
1097+
*/
1098+
(void) nevents;
10371099
#else
1038-
/* Linux: poll on timerfd and UART */
1039-
struct pollfd pfds[2];
1040-
pfds[0] = (struct pollfd){wfi_timer_fd, POLLIN, 0};
1041-
pfds[1] = (struct pollfd){emu->uart.in_fd, POLLIN, 0};
1042-
poll(pfds, 2, -1);
1043-
1044-
/* Consume timerfd event to prevent accumulation */
1045-
if (pfds[0].revents & POLLIN) {
1046-
uint64_t expirations;
1047-
ssize_t ret =
1048-
read(wfi_timer_fd, &expirations, sizeof(expirations));
1049-
(void) ret; /* Ignore read errors - timer will retry */
1050-
}
1100+
/* Linux: configure timerfd with dynamic one-shot timeout */
1101+
struct itimerspec its = {
1102+
.it_interval = {0, 0}, /* One-shot, no repeat */
1103+
.it_value = {wait_ns / 1000000000,
1104+
wait_ns % 1000000000},
1105+
};
1106+
if (timerfd_settime(wfi_timer_fd, 0, &its, NULL) < 0) {
1107+
perror("timerfd_settime");
1108+
/* Continue anyway - will retry next iteration */
1109+
}
1110+
1111+
/* Poll on timerfd and UART */
1112+
struct pollfd pfds[2];
1113+
pfds[0] = (struct pollfd){wfi_timer_fd, POLLIN, 0};
1114+
pfds[1] = (struct pollfd){emu->uart.in_fd, POLLIN, 0};
1115+
poll(pfds, 2, -1);
1116+
1117+
/* Consume timerfd event to prevent accumulation */
1118+
if (pfds[0].revents & POLLIN) {
1119+
uint64_t expirations;
1120+
ssize_t ret = read(wfi_timer_fd, &expirations,
1121+
sizeof(expirations));
1122+
(void) ret; /* Ignore read errors - timer will retry */
1123+
}
10511124
#endif
1125+
}
10521126
}
10531127
}
10541128

0 commit comments

Comments
 (0)