I have a Windows service that uses NetNamedPipe to communicate with other processes on the same machine. It works fine except for one problem: high CPU use. Is there anything I can do to reduce this usage?
To better understand this issue, I made a simple test program that talks to itself over a named pipe and tracks its own CPU use. When using the named pipe infrequently (1 operation per second), CPU use is very low. When using the named pipe frequently (thousands of operations per second), the CPU use increases.
Here is some sample output demonstrating the behaviour. (Note that the CPU uses the Process
> % Processor Time
counter, which is not as simple as the CPU use you might see in Task Manager.)
NetNamedPipe Passed: 31309 Failed: 0 Elapsed: 10.4 s Rate: 3000 Hz Process CPU: 30.0 %
NetNamedPipe Passed: 13 Failed: 0 Elapsed: 11.0 s Rate: 1 Hz Process CPU: 0.9 %
Ideally, I'd like to continue using NetNamedPipe, but do something to reduce the CPU use. I have experimented with tweaking the optional settings of NetNamedPipeBinding using ideas on Stack Overflow and elsewhere, but was unable to reduce the CPU use. Maybe there is something I am missing?
I realise, that quite possibly, I might have to do something more drastic. I might need to send fewer, larger messages, in "bundles". Or I might need to use a different means of inter-process communication. Any advice on what to look into would be appreciated.
My test program code is below. It targets .NET Framework 4.7.2. I have been running on Windows 10.
using System;
using System.Diagnostics;
using System.Threading;
namespace IpcExperiments
{
class Program
{
private static readonly string MyName = "Alice";
private static readonly string ProcessName = "IpcExperiments";
private static readonly double DesiredRate = 3000; // In Hz
// private static readonly double DesiredRate = Double.MaxValue; // Go as fast as possible!
private static PerformanceCounter ProcessCpu = null;
static void Main(string[] args)
{
ProcessCpu = new PerformanceCounter("Process", "% Processor Time", ProcessName);
Test(new Experiments.NetNamedPipe(), MyName, DesiredRate);
// Optionally, add other tests here.
Console.Write("\r ");
Console.WriteLine();
Console.WriteLine("All tests complete! Press Enter to finish.");
Console.ReadLine();
}
private static void Test(Experiments.IIpcExperiment experiment, string myName, double desiredRate = Double.MaxValue)
{
int i = 0;
int successes = 0;
int fails = 0;
double elapsed = 0;
double rate = 0;
double thisCpu = 0;
double avgCpu = 0;
double cpuCount = 0;
string matchingName = String.Format("Hello {0}!", myName);
string experimentName = experiment.GetExperimentName();
Console.Write("\rCreating {0}...", experimentName);
experiment.Setup();
DateTime startTime = DateTime.Now;
DateTime nextCpuRead = DateTime.MinValue;
while (!Console.KeyAvailable)
{
if (experiment.SayHello(myName).Equals(matchingName))
{
successes++;
}
else
{
fails++;
}
if (nextCpuRead < DateTime.Now)
{
thisCpu = ProcessCpu.NextValue();
if (cpuCount == 0)
{
avgCpu = thisCpu;
}
else
{
avgCpu = ((avgCpu * cpuCount) + thisCpu) / (cpuCount + 1);
}
cpuCount++;
nextCpuRead = DateTime.Now.AddSeconds(1);
}
elapsed = (DateTime.Now - startTime).TotalSeconds;
rate = ((double)i) / elapsed;
Console.Write("\r{0}\tPassed: {1}\tFailed: {2}\tElapsed: {3:0.0} s\tRate: {4:0} Hz\t Process CPU: {5:0.0} %"
, experimentName
, successes
, fails
, elapsed
, rate
, avgCpu);
while (rate > desiredRate && !Console.KeyAvailable)
{
Thread.Sleep(1);
elapsed = (DateTime.Now - startTime).TotalSeconds;
rate = ((double)i) / elapsed;
}
i++;
}
Console.ReadKey(true);
Console.WriteLine();
Console.Write("\rDisposing {0}...", experimentName);
experiment.Shutdown();
}
}
}
namespace IpcExperiments.Experiments
{
interface IIpcExperiment
{
string GetExperimentName();
void Setup();
void Shutdown();
string SayHello(string myName);
}
}
using System;
using System.ServiceModel;
namespace IpcExperiments.Experiments
{
[ServiceContract]
public interface INetNamedPipe
{
[OperationContract]
string SayHello(string myName);
}
public class IpcInterface : INetNamedPipe
{
public string SayHello(string myName)
{
return String.Format("Hello {0}!", myName);
}
}
public class NetNamedPipe : IIpcExperiment
{
private ServiceHost Host;
private INetNamedPipe Client;
public void Setup()
{
SetupHost();
SetupClient();
}
public void Shutdown()
{
Host.Close();
}
public string GetExperimentName()
{
return "NetNamedPipe";
}
public string SayHello(string myName)
{
return Client.SayHello(myName);
}
private void SetupHost()
{
Host = new ServiceHost(typeof(IpcInterface),
new Uri[]{
new Uri(@"net.pipe://localhost")
});
NetNamedPipeBinding nnpb = new NetNamedPipeBinding();
Host.AddServiceEndpoint(typeof(INetNamedPipe)
, nnpb
, "NetNamedPipeExample");
Host.Open();
}
private void SetupClient()
{
NetNamedPipeBinding nnpb = new NetNamedPipeBinding();
ChannelFactory<INetNamedPipe> pipeFactory =
new ChannelFactory<INetNamedPipe>(
nnpb,
new EndpointAddress(@"net.pipe://localhost/NetNamedPipeExample"));
Client = pipeFactory.CreateChannel();
}
}
}
Here's how I solved this in the end.
Before the fix, in the sample code in the question above, I made repeated calls to SayHello
and the overhead in doing so consumed a lot of CPU.
After the fix, I am getting the same data through a single Stream
. I suspect that the CPU overhead of setting up a stream is approximately the same, but the stream only needs to be set up once. The overall CPU use is much lower.
Streams are supported by WCF named pipes so I didn't have to abandon using named pipes.
You can read about streaming here, or if that link dies put TransferMode.Streaming
into your favourite search engine.
My stream needed to be "infinite" so it could push data forever, so I needed to make a custom Stream
. This answer on Stack Overflow helped guide me.
I still have some rough edges to work out but the CPU use problem (i.e. the crux of this question) seems to have been solved by this approach.