Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize XXXSelector for many iterations of the event loop #106751

Closed
bdraco opened this issue Jul 14, 2023 · 0 comments · Fixed by #106879
Closed

Optimize XXXSelector for many iterations of the event loop #106751

bdraco opened this issue Jul 14, 2023 · 0 comments · Fixed by #106879
Labels
performance Performance or resource usage stdlib Python modules in the Lib dir

Comments

@bdraco
Copy link
Contributor

bdraco commented Jul 14, 2023

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

@bdraco bdraco added the type-feature A feature request or enhancement label Jul 14, 2023
@bdraco bdraco changed the title Optimize EpollSelector Optimize EpollSelector for many iterations of the event loop Jul 14, 2023
@methane methane added performance Performance or resource usage stdlib Python modules in the Lib dir and removed type-feature A feature request or enhancement labels Jul 18, 2023
methane pushed a commit that referenced this issue Jul 18, 2023
Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
corona10 added a commit to corona10/cpython that referenced this issue Jul 18, 2023
corona10 added a commit to corona10/cpython that referenced this issue Jul 18, 2023
corona10 added a commit to corona10/cpython that referenced this issue Jul 19, 2023
@corona10 corona10 changed the title Optimize EpollSelector for many iterations of the event loop Optimize XXXSelector for many iterations of the event loop Jul 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Performance or resource usage stdlib Python modules in the Lib dir
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants