Skip to content

Updating to .Net Framework / .Net Core 8 from .Net Framework .Net Core 6 seems to come with performance penalties for IO completions #114551

Open
@TimLovellSmith

Description

@TimLovellSmith

This is a somewhat anecdotal description of a performance issue, which I want to post in case anyone can help us 'track it down' based on pattern recognition.

Description

While migrating our app to .Net 8, AND doing other changes, we saw some increases in CPU performance costs, this is probably of magnitude 1-3% CPU overall, that we thought we could try to better track down.

These show where the Delta growths and reductions for us are mostly at:

Image

Image

From the new increase in usage, we can notice the semaphore is now being used in order to gate access to 'GetQueuedCompletionStatus', which seems like it was previously called more often from CompletionPortThreadStart.

I notice other nearby things that are in the new 'growth areas' (in cost) is the 'PortableThreadPool' and 'IOCompletionPoller'.

Also there seems to be an increase in SwapContext, which I guess is switching threads?

Configuration

I think this is Windows Server 2022 OS both before/after. Build may have changed.

Analysis

Looks to me, suspiciously like the main cause of increased CPU cost is something like

  • reconfiguring the managed thread pool? or
  • a new implementation of reading from IO completion ports?
  • updating LIFO semaphore implementation?

When I zoom in on LowLevelLifoSemaphore usage before/after, I see

before: we mostly use 'Wait' and 'WaitForSignal', and its something like 0.1% of total CPU.
after: we mostly use 'Wait' and 'WaitForSignal' but skewed even more towards using 'Wait'. Also now we use 'Release' more than 'WaitForSignal' whereas before it was the other way around. And its something like 0.3% of total CPU.
So, this doesn't seem to explain the entire change in overall CPU cost, but it might be a meaningful part of it.

I see we had a 4x increase in the amount of time in 'SpinWait'. And all of that is from being LowLevelLifoSemaphore.Wait

That sounds possibly a bit like the observations about fairness and unfriendliness to background tasks discussed here, but seen through a different lens?
#103812

In general, our app tends to do a lot of IO processing, and is not usually CPU bound, with many background processes on the machine that occasionally need resources.

I wonder what actually changed to make this difference.
Are there any obvious ways we could get back to spending less CPU?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions