Skip to content

Pool CSteamNetworkingMessage objects instead of new/delete per message - #471

Open
Mohith26 wants to merge 1 commit into
ValveSoftware:masterfrom
Mohith26:message-pool
Open

Mohith26 wants to merge 1 commit into
ValveSoftware:masterfrom
Mohith26:message-pool

Conversation

@Mohith26

@Mohith26 Mohith26 commented Nov 4, 2026

Copy link
Copy Markdown

Addresses #369 and the two FIXME Should avoid this dynamic memory call with some sort of pooling comments in CSteamNetworkingMessage::New() / ReleaseFunc().

Every message sent or received does a new/delete of a ~264-byte CSteamNetworkingMessage. This recycles them:

  • Each thread keeps a cache of up to 64 free objects, so the common alloc/release is a pop/push with no lock.
  • Messages are often allocated on one thread and released on another (received on the service thread, released by the app; or allocated by the app and freed after sending). When a thread's cache is full it moves a batch of 32 to a shared depot, and a thread with an empty cache refills from the depot before going to the heap. The depot is a fixed array (1024 entries) behind a plain std::mutex that is a leaf lock. Anything beyond those bounds is freed as before.
  • When a thread exits, its cached objects go back to the depot. GameNetworkingSockets_Kill() frees the depot and the calling thread's cache.
  • Off in AddressSanitizer builds (pooling would hide use-after-free) and when the old-Apple-clang thread_local fallback applies; can be disabled with -DSTEAMNETWORKINGSOCKETS_ENABLE_MESSAGE_POOL=0.
  • New() also clears m_nUserData now. It was never initialized before (fresh heap memory), but with reuse it would otherwise carry over the previous message's value.

The payload buffer is still a separate malloc, since CSteamNetworkingMessages re-points m_pData/m_pfnFreeData at a header in front of the buffer.

Tests: new message_pool test (added to suite-quick) allocates more messages than the caches hold, scribbles on every user-writable field, and checks recycled messages come back fully initialized, including messages allocated on 4 producer threads and released on 4 consumer threads that then exit. I checked it fails if a field isn't reinitialized. test_connection suite-quick and test_crypto pass on macOS (Apple Silicon, clang, Release), and message_pool/pipe also pass with the pool compiled out.

Benchmark: tests/bench_messages.cpp (built as bench_messages, not run by ctest). Release build, Apple Silicon arm64, 1M iterations, median of 7 runs per process, then median over 13 interleaved processes, pool vs. the same build with -DSTEAMNETWORKINGSOCKETS_ENABLE_MESSAGE_POOL=0:

ns per message before after
AllocateMessage(0) + Release() 21.3 4.8
AllocateMessage(64) + Release() 31.6 14.7
alloc 256, then release 256 (64 B) 30.9 16.0
pipe send+recv, one thread, 64 B, batches of 64 78.2 62.5
pipe send+recv, producer and consumer threads 294.5 305.9

The cross-thread pipe case is dominated by lock contention in the pipe path and is within noise (runs ranged 271-314 before, 272-352 after). I haven't measured the UDP path, where allocation is a smaller share of the per-message cost.

Every message sent or received allocated and freed a CSteamNetworkingMessage
with new/delete (see the FIXMEs in New() and ReleaseFunc(), and ValveSoftware#369).
Recycle them instead: each thread keeps a small cache of free objects, and
batches of 32 move between thread caches and a shared depot (protected by a
leaf mutex) so that messages allocated on one thread and released on another
are reused too.  Caches and the depot are bounded; anything beyond that is
freed.  A thread's cache goes back to the depot when the thread exits, and
GameNetworkingSockets_Kill frees the depot.

Pooling is off in AddressSanitizer builds (it would hide use-after-free) and
can be disabled with STEAMNETWORKINGSOCKETS_ENABLE_MESSAGE_POOL=0.  New() now
also clears m_nUserData, so a recycled object never carries over the previous
message's value.

Adds a message_pool test (to suite-quick) and a bench_messages benchmark.
Sign up for free to 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.

1 participant