Skip to content

Reduce syscall and scheduler overhead in socket accept and close - #11490

Open
lpgauth wants to merge 4 commits into
erlang:masterfrom
lpgauth:esock-fewer-syscalls
Open

Reduce syscall and scheduler overhead in socket accept and close#11490
lpgauth wants to merge 4 commits into
erlang:masterfrom
lpgauth:esock-fewer-syscalls

Conversation

@lpgauth

@lpgauth lpgauth commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

nif_finalize_close always runs on a dirty scheduler, but close() can
only block when SO_LINGER is set with a positive timeout, so every
closed socket pays two scheduler migrations for a case that rarely
applies. Check linger first and only go dirty when the close can
actually block. If the check races with a concurrent close the
getsockopt fails and we just fall back to the dirty path.

The second commit passes SOCK_NONBLOCK to accept4() so the
SET_NONBLOCKING call on every accepted socket can be skipped.

Quick benchmark, HTTP server on the socket backend, 64 concurrent
connections, one request per connection (M2 Pro): 21.6k -> 24.3k req/s.
Keep-alive throughput unchanged. No accept4 on macOS, so that's the
close change alone.

socket_SUITE and socket_api_SUITE pass on macOS, both maint and master.

close() can only block when SO_LINGER is set with a positive
timeout, but nif_finalize_close always runs on a dirty scheduler,
costing two scheduler migrations per closed socket. Check the
linger option and only take the dirty path when close can block.
@CLAassistant

CLAassistant commented Aug 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

CT Test Results

    3 files    136 suites   50m 50s ⏱️
1 693 tests 1 636 ✅ 57 💤 0 ❌
2 336 runs  2 261 ✅ 75 💤 0 ❌

Results for commit 9b0fdcf.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

Passing SOCK_NONBLOCK to accept4() makes the SET_NONBLOCKING
call on every accepted socket redundant, saving a syscall per
accept on platforms that have accept4().
Every setsockopt went through a save and restore of both SO_PRIORITY
and IP_TOS, on the grounds that "if any other option is set after tos,
tos might be zeroed" - a comment carried over from the inet driver.
That costs two getsockopt calls and up to two more setsockopt calls on
every option set.

Only half of it holds. Setting IP_TOS does make the kernel derive a new
SO_PRIORITY from it, so that one option has to put the priority back.
Setting SO_PRIORITY leaves the tos alone, and so does setting anything
else: verified on Linux 7.0 with TCP_NODELAY, SO_RCVBUF and SO_LINGER,
none of which disturbed either value.

So do it only for IP_TOS, and only for the priority. Setting an option
on an accepted connection goes from four system calls to one. The
socket options a connection sets still look independent to the user,
which is what the dance was there for.
@lpgauth
lpgauth force-pushed the esock-fewer-syscalls branch from a48655d to 6be6a8e Compare August 16, 2026 23:57
@IngelaAndin IngelaAndin added the team:VM Assigned to OTP team VM label Aug 17, 2026
@jhogberg jhogberg added team:PS Assigned to OTP team PS and removed team:VM Assigned to OTP team VM labels Aug 17, 2026
@lpgauth

lpgauth commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

These patches have been running in production for several days with no issues. I can share more benchmark numbers if that's useful.

if (descP->sock == INVALID_SOCKET)
return FALSE;

if (sock_getopt(descP->sock, SOL_SOCKET, SO_LINGER,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if SO_LINGER is not defined on a given platform? I see other places in this file ifdef-ing on the macro's existence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9b0fdcf. When the macro is missing the close can't linger, so it skips the dirty scheduler. Also switched the length to SOCKOPTLEN_T while at it, SOCKLEN_T falls back to size_t on Windows where getsockopt wants an int.

SO_LINGER is guarded everywhere else in the file, so guard the new
check too. Without the option a close cannot linger, so the dirty
scheduler is skipped. Also use SOCKOPTLEN_T for the option length
like every other getsockopt call in the file; SOCKLEN_T falls back
to size_t on Windows where getsockopt writes through an int pointer.
@lpgauth

lpgauth commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Pushed the SO_LINGER guard as a separate commit, will squash before merge.

Also worth noting the Windows and OpenBSD CI failures here aren't related to the change: the Windows job died setting up wxWidgets before compiling anything, and the OpenBSD VM hung mid-build in megaco.

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

Labels

team:PS Assigned to OTP team PS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants