Skip to content

Optimize XXXSelector for many iterations of the event loop #106751

@bdraco

Description

@bdraco

split out from #106555 (comment)

The current EpollSelector can be sped up a bit. This makes quite a difference when there are 100000+ iterations of the event loop per minute (the use case being receiving bluetooth data from multiple sources) since selectors have to run every iteration.

original: 11.831302762031555
new: 9.579423972172663

import timeit
import math
import select
import os
from selectors import EpollSelector, EVENT_WRITE, EVENT_READ


class OriginalEpollSelector(EpollSelector):
    def select(self, timeout=None):
        if timeout is None:
            timeout = -1
        elif timeout <= 0:
            timeout = 0
        else:
            # epoll_wait() has a resolution of 1 millisecond, round away
            # from zero to wait *at least* timeout seconds.
            timeout = math.ceil(timeout * 1e3) * 1e-3
        # epoll_wait() expects `maxevents` to be greater than zero;
        # we want to make sure that `select()` can be called when no
        # FD is registered.
        max_ev = max(len(self._fd_to_key), 1)
        ready = []
        try:
            fd_event_list = self._selector.poll(timeout, max_ev)
        except InterruptedError:
            return ready
        for fd, event in fd_event_list:
            events = 0
            if event & ~select.EPOLLIN:
                events |= EVENT_WRITE
            if event & ~select.EPOLLOUT:
                events |= EVENT_READ

            key = self._key_from_fd(fd)
            if key:
                ready.append((key, events & key.events))
        return ready


NOT_EPOLLIN = ~select.EPOLLIN
NOT_EPOLLOUT = ~select.EPOLLOUT

class NewEpollSelector(EpollSelector):
    def select(self, timeout=None):
        if timeout is None:
            timeout = -1
        elif timeout <= 0:
            timeout = 0
        else:
            # epoll_wait() has a resolution of 1 millisecond, round away
            # from zero to wait *at least* timeout seconds.
            timeout = math.ceil(timeout * 1e3) * 1e-3
        # epoll_wait() expects `maxevents` to be greater than zero;
        # we want to make sure that `select()` can be called when no
        # FD is registered.
        max_ev = len(self._fd_to_key) or 1
        ready = []
        try:
            fd_event_list = self._selector.poll(timeout, max_ev)
        except InterruptedError:
            return ready
        
        fd_to_key = self._fd_to_key
        for fd, event in fd_event_list:
            key = fd_to_key.get(fd)
            if key:
                ready.append(
                    (
                        key,
                        (
                            (event & NOT_EPOLLIN and EVENT_WRITE)
                            | (event & NOT_EPOLLOUT and EVENT_READ)
                        )
                        & key.events,
                    )
                )
        return ready


original_epoll = OriginalEpollSelector()
new_epoll = NewEpollSelector()


for _ in range(512):
    r, w = os.pipe()
    os.write(w, b"a")
    original_epoll.register(r, EVENT_READ)
    new_epoll.register(r, EVENT_READ)


original_time = timeit.timeit(
    "selector.select()",
    number=100000,
    globals={"selector": original_epoll},
)
new_time = timeit.timeit(
    "selector.select()",
    number=100000,
    globals={"selector": new_epoll},
)

print("original: %s" % original_time)
print("new: %s" % new_time)

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directory

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions