読者です 読者をやめる 読者になる 読者になる

CORBAからWCFへ(性能比較編)

CORBA WCF C#

ちょっと古いけど、興味深い記事を発見した。

Windows Communication Foundation (WCF) と既存の分散通信テクノロジのパフォーマンスの比較

今では.NET 3.5 SP1がリリースされてパフォーマンスも改善されているだろうから、どこまで参考になるのか分からないけど、この記事によると、既存のWebサービスや.NETリモーティングと比較して、WCFのパフォーマンスは高いらしい。

では、CORBAと比較するとどうなのだろう?
というわけで、ちょっと実験してみた。

ただし、元記事のようにネットワークのボトルネックやCPUの負荷率の影響などは考慮していないし、こういう実験方法のセオリーもあまり知らないので、信頼性はあまりないかも。

実験方法

  • 通信方式は以下の4つ
    • WCFのWSHttpBinding
    • WCFのNetTcpBinding
    • WCFのNetNamedPipeBinding
    • CORBA
  • 各通信方式で100バイトと1Mバイトのデータを送信して、その通信速度の平均値を出す。
  • プロセス間の通信とPC間の通信でやってみた。
  • WCFではホスティング方法によってパフォーマンスに影響がでるみたいだけど、今回は通常のコンソールアプリケーションとした。
  • CORBA実装としては、IIOP.NETを利用した。
  • PCは、Core2 Duo 2.66GHz メモリ4GBのWindows Vistaマシンと、Let's note W5(Windows XP)。
  • ネットワークは100BASE
  • 結果の縦軸は通信速度(KB/s)なので値の大きい方がパフォーマンスが良いということ。ただし対数表示になっているので注意。

異なるプロセス間での通信

まずは、1台のPC上でサーバとクライアントを動かした場合の結果。

通信方式 100B 1MB
WSHttp(WCF) 174 31480
NetTcp(WCF) 444 251610
IIOP.NET(CORBA) 2583 249840
NamedPipe(WCF) 397 516017

ここで面白い結果がでた。

1MBのデータの送受信の場合は、名前付きパイプ(NetNamedPipeBinding)がNetTcpBindingやIIOP.NETの2倍くらい早いんだけど、100Bのデータのときは、逆転してIIOP.NETの方が早くなっている。

データサイズが小さいと、メッセージ変換(シリアライズ/デシリアライズ、マーシャリング/アンマーシャリング)の回数が増えるから、その影響が出ているのだろう。

CORBAでは送信するバイト列をそのままパケットにコピーしているだけで、WCFでは一旦XMLに変換しているため、こんな差が出たのかなと予想してみる。

異なるPC間での通信

続いて、デスクトップPC側にサーバプログラムを起動して、ノートPCからクライアントプログラムを起動してみた。

通信方式 100B 1MB
WSHttp(WCF) 111 13290
NetTcp(WCF) 1315 82168
IIOP.NET(CORBA) 777 80219

プロセス間通信とは異なり、NetTcpBindingがIIOP.NETを逆転した。

これはどういうことだろう?

IIOP.NETがプロセス間通信のときに上手く最適化しているのか、NetTcpBindingのネットワーク通信の最適化が効いているのか。

利用したコード

  • IIOP.NETのIDL
typedef sequence<octet> OctetSeq;

interface IRemoteObject
{
  OctetSeq GetBytes(in long numBytes);
};
  • IIOP.NETのサーバ
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Threading;
using Ch.Elca.Iiop;

namespace IiopServer
{
    class RemoteObjectImpl : MarshalByRefObject, IRemoteObject
    {
        public byte[] GetBytes(int numBytes)
        {
            return new byte[numBytes];
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int port = 8087;
            IiopChannel channel = new IiopChannel(port);
            ChannelServices.RegisterChannel(channel, false);

            RemoteObjectImpl ro = new RemoteObjectImpl();
            string uri = "RemoteObject";
            RemotingServices.Marshal(ro, uri);

            Console.WriteLine("サーバの起動完了しました。");
            Thread.Sleep(Timeout.Infinite);
        }
    }
}
  • IIOP.NETのクライアント
using System;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using Ch.Elca.Iiop;

namespace IiopClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string hostname = "localhost";
            int port = 8087;

            Console.WriteLine("開始するには、何かキーを押してください。");
            Console.ReadLine();

            IiopClientChannel channel = new IiopClientChannel();
            ChannelServices.RegisterChannel(channel, false);

            IRemoteObject client = (IRemoteObject)RemotingServices.Connect(typeof(IRemoteObject), "iiop://" + hostname + ":" + port + "/RemoteObject");

            CheckPerformance(client, 1000000, 100);
            Console.ReadLine();
        }

        static void CheckPerformance(IRemoteObject client, int datasize, int num)
        {
            // 試し呼び
            client.GetBytes(datasize);

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            for (int i = 0; i < num; i++)
            {
                client.GetBytes(datasize);
            }

            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedMilliseconds);
        }
    }
}
  • WCFのサーバ
using System;
using System.ServiceModel;
using System.Threading;

namespace WcfServer
{
    class Program
    {
        [ServiceContract]
        public interface IRemoteObject
        {
            [OperationContract]
            byte[] GetBytes(int numBytes);
        }

        [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
        public class RemoteObjectImpl : IRemoteObject
        {
            public byte[] GetBytes(int numBytes)
            {
                return new byte[numBytes];
            }
        }

        static void Main(string[] args)
        {
            string hostname = "localhost";
            
            CreateWSHttpServer(hostname);
            CreateNetTcpServer(hostname);
            CreateNetNamedPipeServer();

            Console.WriteLine("サーバの起動完了しました。");
            Thread.Sleep(Timeout.Infinite);
        }

        static void CreateWSHttpServer(string hostname)
        {
            WSHttpBinding binding = new WSHttpBinding();
            binding.MaxReceivedMessageSize = 2000000;
            binding.ReaderQuotas.MaxArrayLength = 2000000;

            Uri uri = new Uri("http://" + hostname + ":8000/RemoteObject");

            ServiceHost host = new ServiceHost(typeof(RemoteObjectImpl));

            host.AddServiceEndpoint(typeof(IRemoteObject), binding, uri);
            host.Open();
        }

        static void CreateNetTcpServer(string hostname)
        {
            NetTcpBinding binding = new NetTcpBinding();
            binding.MaxReceivedMessageSize = 2000000;
            binding.ReaderQuotas.MaxArrayLength = 2000000;

            Uri uri = new Uri("net.tcp://" + hostname + ":8001/RemoteObject");

            ServiceHost host = new ServiceHost(typeof(RemoteObjectImpl));

            host.AddServiceEndpoint(typeof(IRemoteObject), binding, uri);
            host.Open();
        }

        static void CreateNetNamedPipeServer()
        {
            NetNamedPipeBinding binding = new NetNamedPipeBinding();
            binding.MaxReceivedMessageSize = 2000000;
            binding.ReaderQuotas.MaxArrayLength = 2000000;

            Uri uri = new Uri("net.pipe://localhost/RemoteObject");

            ServiceHost host = new ServiceHost(typeof(RemoteObjectImpl));

            host.AddServiceEndpoint(typeof(IRemoteObject), binding, uri);
            host.Open();
        }
    }
}
  • WCFのクライアント
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.ServiceModel;

namespace WcfClient
{
    class Program
    {
        [ServiceContract]
        public interface IRemoteObject
        {
            [OperationContract]
            byte[] GetBytes(int numBytes);
        }

        static void Main(string[] args)
        {
            Console.WriteLine("開始するには、何かキーを押してください。");
            Console.ReadLine();

            string hostname = "localhost";

            List<IRemoteObject> clients = new List<IRemoteObject>(){
                CreateWSHttpClient(hostname),
                CreateNetTcpClient(hostname),
                CreateNetNamedPipeClient()
            };

            clients.ForEach(client => CheckPerformance(client, 1000000, 100));

            Console.ReadLine();
        }

        static void CheckPerformance(IRemoteObject client, int datasize, int num)
        {
            // 試し呼び
            client.GetBytes(datasize);

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            for (int i = 0; i < num; i++)
            {
                client.GetBytes(datasize);
            }

            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedMilliseconds);
        }
    }
}

(おまけ)WCFで大きなサイズのデータを送信する場合

WCFで65536バイト以上のデータを送受信しようとするとエラーが発生するので、以下のようにバッファサイズを大きくしてやる必要がある。

binding.MaxReceivedMessageSize = 2000000;
binding.ReaderQuotas.MaxArrayLength = 2000000;

なお、ReaderQuotasにアクセスするためには、System.Runtime.Serializationのアセンブリを参照に追加する必要がある。(一部のプロパティだけ別アセンブリってことができるのか・・・)