Skip to content

Commit 2650b61

Browse files
committed
Don't hit epoll unless a scheduler absolutely must
Currently, a scheduler will hit epoll() or kqueue() at the end of *every task*. The reason is that the scheduler will context switch back to the scheduler task, terminate the previous task, and then return from run_sched_once. In doing so, the scheduler will poll for any active I/O. This shows up painfully in benchmarks that have no I/O at all. For example, this benchmark: for _ in range(0, 1000000) { spawn(proc() {}); } In this benchmark, the scheduler is currently wasting a good chunk of its time hitting epoll() when there's always active work to be done (run with RUST_THREADS=1). This patch uses the previous two commits to alter the scheduler's behavior to only return from run_sched_once if no work could be found when trying really really hard. If there is active I/O, this commit will perform the same as before, falling back to epoll() to check for I/O completion (to not starve I/O tasks). In the benchmark above, I got the following numbers: 12.554s on today's master 3.861s with #12172 applied 2.261s with both this and #12172 applied cc #8341
1 parent 4256d24 commit 2650b61

File tree

1 file changed

+50
-19
lines changed

1 file changed

+50
-19
lines changed

src/libgreen/sched.rs

+50-19
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,23 @@ impl Scheduler {
252252

253253
// * Execution Functions - Core Loop Logic
254254

255-
// The model for this function is that you continue through it
256-
// until you either use the scheduler while performing a schedule
257-
// action, in which case you give it away and return early, or
258-
// you reach the end and sleep. In the case that a scheduler
259-
// action is performed the loop is evented such that this function
260-
// is called again.
255+
// This function is run from the idle callback on the uv loop, indicating
256+
// that there are no I/O events pending. When this function returns, we will
257+
// fall back to epoll() in the uv event loop, waiting for more things to
258+
// happen. We may come right back off epoll() if the idle callback is still
259+
// active, in which case we're truly just polling to see if I/O events are
260+
// complete.
261+
//
262+
// The model for this function is to execute as much work as possible while
263+
// still fairly considering I/O tasks. Falling back to epoll() frequently is
264+
// often quite expensive, so we attempt to avoid it as much as possible. If
265+
// we have any active I/O on the event loop, then we're forced to fall back
266+
// to epoll() in order to provide fairness, but as long as we're doing work
267+
// and there's no active I/O, we can continue to do work.
268+
//
269+
// If we try really hard to do some work, but no work is available to be
270+
// done, then we fall back to epoll() to block this thread waiting for more
271+
// work (instead of busy waiting).
261272
fn run_sched_once(mut ~self, stask: ~GreenTask) {
262273
// Make sure that we're not lying in that the `stask` argument is indeed
263274
// the scheduler task for this scheduler.
@@ -269,23 +280,43 @@ impl Scheduler {
269280

270281
// First we check for scheduler messages, these are higher
271282
// priority than regular tasks.
272-
let (sched, stask, did_work) =
283+
let (mut sched, mut stask, mut did_work) =
273284
self.interpret_message_queue(stask, DontTryTooHard);
274-
if did_work {
275-
return stask.put_with_sched(sched);
276-
}
277285

278-
// This helper will use a randomized work-stealing algorithm
279-
// to find work.
280-
let (sched, stask, did_work) = sched.do_work(stask);
281-
if did_work {
282-
return stask.put_with_sched(sched);
286+
// After processing a message, we consider doing some more work on the
287+
// event loop. The "keep going" condition changes after the first
288+
// iteration becase we don't want to spin here infinitely.
289+
//
290+
// Once we start doing work we can keep doing work so long as the
291+
// iteration does something. Note that we don't want to starve the
292+
// message queue here, so each iteration when we're done working we
293+
// check the message queue regardless of whether we did work or not.
294+
let mut keep_going = !did_work || !sched.event_loop.has_active_io();
295+
while keep_going {
296+
let (a, b, c) = match sched.do_work(stask) {
297+
(sched, task, false) => {
298+
sched.interpret_message_queue(task, GiveItYourBest)
299+
}
300+
(sched, task, true) => {
301+
let (sched, task, _) =
302+
sched.interpret_message_queue(task, GiveItYourBest);
303+
(sched, task, true)
304+
}
305+
};
306+
sched = a;
307+
stask = b;
308+
did_work = c;
309+
310+
// We only keep going if we managed to do something productive and
311+
// also don't have any active I/O. If we didn't do anything, we
312+
// should consider going to sleep, and if we have active I/O we need
313+
// to poll for completion.
314+
keep_going = did_work && !sched.event_loop.has_active_io();
283315
}
284316

285-
// Now, before sleeping we need to find out if there really
286-
// were any messages. Give it your best!
287-
let (mut sched, stask, did_work) =
288-
sched.interpret_message_queue(stask, GiveItYourBest);
317+
// If we ever did some work, then we shouldn't put our scheduler
318+
// entirely to sleep just yet. Leave the idle callback active and fall
319+
// back to epoll() to see what's going on.
289320
if did_work {
290321
return stask.put_with_sched(sched);
291322
}

0 commit comments

Comments
 (0)