Skip to content

gh-106554: Replace _BaseSelectorImpl._key_from_fd with a simple get - #106555

Merged
methane merged 7 commits into
python:mainfrom
bdraco:unused__key_from_fd
Jul 14, 2023
Merged

gh-106554: Replace _BaseSelectorImpl._key_from_fd with a simple get#106555
methane merged 7 commits into
python:mainfrom
bdraco:unused__key_from_fd

Conversation

@bdraco

@bdracobdraco commented Jul 9, 2023

Copy link
Copy Markdown
Contributor

`_key_from_fd` re-implemented `.get()` and can be removed
@bdraco
bdraco marked this pull request as ready for review July 9, 2023 02:33
@bdraco

Copy link
Copy Markdown
ContributorAuthor
importtimeitimportmathimportselectimportosfromselectorsimportEpollSelector, EVENT_WRITE, EVENT_READclassOriginalEpollSelector(EpollSelector):
defselect(self, timeout=None):
iftimeoutisNone:
timeout=-1eliftimeout<=0:
timeout=0else:
# 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)
exceptInterruptedError:
returnreadyforfd, eventinfd_event_list:
events=0ifevent&~select.EPOLLIN:
events|=EVENT_WRITEifevent&~select.EPOLLOUT:
events|=EVENT_READkey=self._key_from_fd(fd)
ifkey:
ready.append((key, events&key.events))
returnreadyclassNewEpollSelector(EpollSelector):
defselect(self, timeout=None):
iftimeoutisNone:
timeout=-1eliftimeout<=0:
timeout=0else:
# 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)
exceptInterruptedError:
returnreadyforfd, eventinfd_event_list:
events=0ifevent&~select.EPOLLIN:
events|=EVENT_WRITEifevent&~select.EPOLLOUT:
events|=EVENT_READkey=self._fd_to_key.get(fd)
ifkey:
ready.append((key, events&key.events))
returnreadyoriginal_epoll=OriginalEpollSelector()
new_epoll=NewEpollSelector()
for_inrange(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)

@bdraco

Copy link
Copy Markdown
ContributorAuthor

512 pipes

original: 9.91608710300352
new: 9.240273159986828

1024 pipes

original: 20.881464588004746
new: 19.727144259988563

@bdraco

bdraco commented Jul 12, 2023

Copy link
Copy Markdown
ContributorAuthor

A more aggressive change to reduce the repeated calls inside the loop could shave some more time off

512 pipes

original: 10.050354178994894
new: 8.222623112000292

1024 pipes

original: 20.87793983600568
new: 17.375853078992805

something like the below (but outside the scope here)

NOT_EPOLLIN=~select.EPOLLINNOT_EPOLLOUT=~select.EPOLLOUTclassNewEpollSelector(EpollSelector):
defselect(self, timeout=None):
iftimeoutisNone:
timeout=-1eliftimeout<=0:
timeout=0else:
# 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)
exceptInterruptedError:
returnreadyforfd, eventinfd_event_list:
key=self._fd_to_key.get(fd)
ifkey:
ready.append(
(
key,
(
(event&NOT_EPOLLINandEVENT_WRITE)
| (event&NOT_EPOLLOUTandEVENT_READ)
)
&key.events,
)
)
returnready

Getting rid of the max and writing it as len(self._fd_to_key) or 1 would probably help as well

Screenshot 2023-07-13 at 3 32 14 PM
NOT_EPOLLIN=~select.EPOLLINNOT_EPOLLOUT=~select.EPOLLOUTclassNewEpollSelector(EpollSelector):
defselect(self, timeout=None):
iftimeoutisNone:
timeout=-1eliftimeout<=0:
timeout=0else:
# 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) or1ready= []
try:
fd_event_list=self._selector.poll(timeout, max_ev)
exceptInterruptedError:
returnreadyforfd, eventinfd_event_list:
key=self._fd_to_key.get(fd)
ifkey:
ready.append(
(
key,
(
(event&NOT_EPOLLINandEVENT_WRITE)
| (event&NOT_EPOLLOUTandEVENT_READ)
)
&key.events,
)
)
returnready

at 512 (on a different system)

original: 11.723339454038069
new: 9.671272879000753

@bdraco

Copy link
Copy Markdown
ContributorAuthor

KqueueSelector could shave off some more time as well with something like

defselect(self, timeout=None):
timeout=NoneiftimeoutisNoneelsemax(timeout, 0)
# If max_ev is 0, kqueue will ignore the timeout. For consistent# behavior with the other selector classes, we prevent that here# (using max). See https://bugs.python.org/issue29255max_ev=max(len(self._fd_to_key), 1)
ready= []
try:
kev_list=self._selector.control(None, max_ev, timeout)
exceptInterruptedError:
returnreadyforkevinkev_list:
key=self._fd_to_key.get(kev.ident)
ifkey:
flag=kev.filterifflag==select.KQ_FILTER_READ:
events=EVENT_READelifflag==select.KQ_FILTER_WRITE:
events=EVENT_WRITEelse:
events=0ready.append((key, events&key.events))
returnready

Comment threadMisc/NEWS.d/next/Library/2023-07-09-01-59-24.gh-issue-106554.37c53J.rst Outdated
Comment threadMisc/NEWS.d/next/Library/2023-07-09-01-59-24.gh-issue-106554.37c53J.rst Outdated
Comment threadMisc/NEWS.d/next/Library/2023-07-09-01-59-24.gh-issue-106554.37c53J.rst Outdated
@bdraco

Copy link
Copy Markdown
ContributorAuthor

With #106555 (comment) the selector overhead falls below the cost of socket.recv in the profiles. Its still high on the list at #4 for my home assistant install, eclipsed only by socket.recv, _run_once and some bluetooth overhead.

Comment threadMisc/NEWS.d/next/Library/2023-07-09-01-59-24.gh-issue-106554.37c53J.rst Outdated
@methane
methane merged commit aeef859 into python:mainJul 14, 2023
@bdraco

Copy link
Copy Markdown
ContributorAuthor

Thanks. Will work on the follow ups from above

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@bdraco@methane@bedevere-bot