Skip to content

Optimize XXXSelector for many iterations of the event loop #106751

Closed
@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 usagestdlibPython modules in the Lib dir

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions