R – seeing slower performance here from WCF than Remoting

netperformanceremotingwcf

Everything I'm told says that WCF should be at least as fast as remoting. I have a specific scenario here, however, where it isn't even close, and I'm wondering if someone can spot something obvious that I'm doing wrong. I'm looking into the possibility of replacing remoting with wcf for the in-process intra-appdomain communication heavy lifting. Here's the code:

[ServiceContract]
interface IWorkerObject
{
    [OperationContract] Outcome DoWork(Input t);
}

[DataContract]
[Serializable]
class Input
{
    [DataMember] public int TaskId { get; set; }
    [DataMember] public int ParentTaskId { get; set; }
    [DataMember] public DateTime DateCreated { get; set; }
    [DataMember] public string TextData { get; set; }
    [DataMember] public byte[] BinaryData { get; set; }
}

[DataContract]
[Serializable]
class Outcome
{
    [DataMember] public string Result { get; set; }
    [DataMember] public string TextData { get; set; }
    [DataMember] public byte[] BinaryData { get; set; }

}

class Program
{
    static void Main(string[] args)
    {
        run_rem_test();
        run_wcf_test();
        run_rem_test();
        run_wcf_test();
    }

    static void run_rem_test()
    {
        var dom = AppDomain.CreateDomain("remoting domain", null);
        var obj = dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as IWorkerObject;

        RunTest("remoting", obj);

        AppDomain.Unload(dom);
    }

    static void run_wcf_test()
    {
        var dom  = AppDomain.CreateDomain("wcf domain", null);
        var dcnt = dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as WorkerObject;
        var fact = new ChannelFactory<IWorkerObject>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
        var chan = fact.CreateChannel();

        dcnt.OpenChannel();

        RunTest("wcf", chan);

        fact.Close();

        dcnt.CloseChannel();

        AppDomain.Unload(dom);
    }

    static void RunTest(string test, IWorkerObject dom)
    {
        var t = new Input()
        {
            TextData     = new string('a', 8192),
            BinaryData   = null,
            DateCreated  = DateTime.Now,
            TaskId       = 12345,
            ParentTaskId = 12344,
        };

        var sw = System.Diagnostics.Stopwatch.StartNew();

        for( var i = 0; i < 1000; i++ )
            dom.DoWork(t);

        sw.Stop();

        Console.WriteLine("{1} test run in {0}ms", sw.ElapsedMilliseconds, test);
    }

}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class WorkerObject : MarshalByRefObject, IWorkerObject
{
    ServiceHost m_host;

    public void OpenChannel()
    {
        m_host = new ServiceHost(this);

        m_host.AddServiceEndpoint(typeof(IWorkerObject), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");

        m_host.Open();
    }

    public void CloseChannel()
    {
        m_host.Close();
    }

    public Outcome DoWork(Input t)
    {
        return new Outcome()
        {
            TextData   = new string('b', 8192),
            BinaryData = new byte[1024],
            Result     = "the result",
        };
    }

}

When I run this code I get numbers that look like this:

remoting test run in 386ms
wcf test run in 3467ms
remoting test run in 499ms
wcf test run in 1840ms

UPDATE: So it turns out that it's just the initial setup that is so costly for WCF (Thanks, Zach!). Because I was recreating the AppDomains in each test, I was paying that price over and over. Here's the updated code:

[ServiceContract]
interface IWorkerObject
{
    [OperationContract] Outcome DoWork(Input t);
}

[DataContract]
[Serializable]
class Input
{
    [DataMember] public int TaskId { get; set; }
    [DataMember] public int ParentTaskId { get; set; }
    [DataMember] public DateTime DateCreated { get; set; }
    [DataMember] public string TextData { get; set; }
    [DataMember] public byte[] BinaryData { get; set; }
}

[DataContract]
[Serializable]
class Outcome
{
    [DataMember] public string Result { get; set; }
    [DataMember] public string TextData { get; set; }
    [DataMember] public byte[] BinaryData { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var rem_dom = AppDomain.CreateDomain("remoting domain", null);
        var rem_obj = rem_dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as IWorkerObject;

        var wcf_dom = AppDomain.CreateDomain("wcf domain", null);
        var mgr_obj = wcf_dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as WorkerObject;
        var fact    = new ChannelFactory<IWorkerObject>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
        var wcf_obj = fact.CreateChannel();

        var rem_tot = 0L;
        var wcf_tot = 0L;

        mgr_obj.OpenChannel();

        for( var i = 0; i < 10; i++ )
        {
            rem_tot += RunTest("remoting", i, rem_obj);
            wcf_tot += RunTest("wcf",      i, wcf_obj);
        }

        fact.Close();

        mgr_obj.CloseChannel();

        AppDomain.Unload(rem_dom);
        AppDomain.Unload(wcf_dom);

        Console.WriteLine();
        Console.WriteLine("remoting total: {0}", rem_tot);
        Console.WriteLine("wcf total:      {0}", wcf_tot);
    }

    static long RunTest(string test, int iter, IWorkerObject dom)
    {
        var t = new Input()
        {
            TextData     = new string('a', 8192),
            BinaryData   = null,
            DateCreated  = DateTime.Now,
            TaskId       = 12345,
            ParentTaskId = 12344,
        };

        var sw = System.Diagnostics.Stopwatch.StartNew();

        for( var i = 0; i < 1000; i++ )
            dom.DoWork(t);

        sw.Stop();

        Console.WriteLine("{1,-8} {2,2} test run in {0}ms", sw.ElapsedMilliseconds, test, iter);

        return sw.ElapsedMilliseconds;
    }

}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class WorkerObject : MarshalByRefObject, IWorkerObject
{
    ServiceHost m_host;

    public void OpenChannel()
    {
        m_host = new ServiceHost(typeof(WorkerObject));

        m_host.AddServiceEndpoint(typeof(IWorkerObject), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");

        m_host.Open();
    }

    public void CloseChannel()
    {
        m_host.Close();
    }

    public Outcome DoWork(Input t)
    {
        return new Outcome()
        {
            TextData   = new string('b', 8192),
            BinaryData = new byte[1024],
            Result     = "the result",
        };
    }

}

This code gives numbers like this:

remoting  0 test run in 377ms
wcf       0 test run in 2255ms
remoting  1 test run in 488ms
wcf       1 test run in 353ms
remoting  2 test run in 507ms
wcf       2 test run in 355ms
remoting  3 test run in 495ms
wcf       3 test run in 351ms
remoting  4 test run in 484ms
wcf       4 test run in 344ms
remoting  5 test run in 484ms
wcf       5 test run in 354ms
remoting  6 test run in 483ms
wcf       6 test run in 346ms
remoting  7 test run in 491ms
wcf       7 test run in 347ms
remoting  8 test run in 485ms
wcf       8 test run in 358ms
remoting  9 test run in 494ms
wcf       9 test run in 338ms

remoting total: 4788
wcf total:      5401

Best Answer

The Notes

For the most part, the time is spent setting up and tearing down the WCF channel. The other lions share of time appears to be consumed by running within a debugger. Keep in mind this is really informal type testing.. :)

I was able to reproduce your numbers, so i started from there. This showed WCF numbers were roughly 3xs greater than the remoting numbers.

By caching the channel factory (and respective remoting objects), the numbers dropped so that WCF was only 2x's greater than the remoting numbers.

After trying a dozen other different tweaks, the only one that seemed to shave any real measurable time off the numbers was to clear all the debug service behaviors (added by default) - this resulted in roughly ~100ms. Not enough to really get excited about.

Then, on a whim, I ran the code outside the debugger, in a release configuration. The numbers dropped to roughly the equivalent of the remoting numbers, if not better. Forehead smacked the desk and called it done.

In a nutshell, you should see roughly the same, with a possibility of better performance using WCF and gain a better programming model to boot.

Sample Run

remoting 1 test run in 347ms    
wcf      1 test run in 1544ms    
remoting 2 test run in 493ms    
wcf      2 test run in 324ms
remoting 3 test run in 497ms
wcf      3 test run in 336ms
remoting 4 test run in 449ms
wcf      4 test run in 289ms
remoting 5 test run in 448ms
wcf      5 test run in 284ms
remoting 6 test run in 447ms
wcf      6 test run in 282ms
remoting 7 test run in 439ms
wcf      7 test run in 281ms
remoting 8 test run in 441ms
wcf      8 test run in 278ms
remoting 9 test run in 441ms
wcf      9 test run in 278ms
remoting 10 test run in 438ms
wcf      10 test run in 286ms

The Code

Note - this code has been completely bastardized. I apologize. :)

using System;
using System.ServiceModel;
using System.Runtime.Serialization;



[ServiceContract]
interface IWorkerObject
{
    [OperationContract]
    Outcome DoWork(Input t);
}

[DataContract]
[Serializable]
class Input
{
    [DataMember]
    public int TaskId { get; set; }
    [DataMember]
    public int ParentTaskId { get; set; }
    [DataMember]
    public DateTime DateCreated { get; set; }
    [DataMember]
    public string TextData { get; set; }
    [DataMember]
    public byte[] BinaryData { get; set; }
}

[DataContract]
[Serializable]
class Outcome
{
    [DataMember]
    public string Result { get; set; }
    [DataMember]
    public string TextData { get; set; }
    [DataMember]
    public byte[] BinaryData { get; set; }

}

class Program
{
    static AppDomain dom = AppDomain.CreateDomain("wcf domain", null);
    static WorkerObject dcnt = dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as WorkerObject;
    static ChannelFactory<IWorkerObject> fact = new ChannelFactory<IWorkerObject>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
    static IWorkerObject chan = fact.CreateChannel();
    static AppDomain remdom = AppDomain.CreateDomain("remoting domain", null);
    static IWorkerObject remoteObject;

    static void Main(string[] args)
    {

        remoteObject = remdom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as IWorkerObject;


        dcnt.OpenChannel();


        int numberOfIterations = 10;

        for (int i = 1; i <= numberOfIterations; i++)
        {
            run_rem_test(i);
            run_wcf_test(i);
        }

        fact.Close();

        dcnt.CloseChannel();

        AppDomain.Unload(dom);
        AppDomain.Unload(remdom);



    }

    static void run_rem_test(int iteration)
    {

        RunTest("remoting " + iteration, remoteObject);


    }

    static void run_wcf_test(int iteration)
    {


        RunTest("wcf      " + iteration, chan);

    }

    static void RunTest(string test, IWorkerObject dom)
    {
        var t = new Input()
        {
            TextData = new string('a', 8192),
            BinaryData = null,
            DateCreated = DateTime.Now,
            TaskId = 12345,
            ParentTaskId = 12344,
        };

        var sw = System.Diagnostics.Stopwatch.StartNew();

        for (var i = 0; i < 1000; i++)
            dom.DoWork(t);

        sw.Stop();

        Console.WriteLine("{1} test run in {0}ms", sw.ElapsedMilliseconds, test);
    }

}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class WorkerObject : MarshalByRefObject, IWorkerObject
{
    ServiceHost m_host;

    public void OpenChannel()
    {
        m_host = new ServiceHost(typeof(WorkerObject));


        m_host.AddServiceEndpoint(typeof(IWorkerObject), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");

        // cache our ServiceBehaviorAttribute, clear all other behaviors (mainly debug)
        // and add our ServiceBehavior back
        //
        var b = m_host.Description.Behaviors[0] as ServiceBehaviorAttribute;


        m_host.Description.Behaviors.Clear();

        m_host.Description.Behaviors.Add(b);



        m_host.Open();
    }

    public void CloseChannel()
    {
        m_host.Close();
    }

    public Outcome DoWork(Input t)
    {
        return new Outcome()
        {
            TextData = new string('b', 8192),
            BinaryData = new byte[1024],
            Result = "the result",
        };
    }

}