はじめに
以前 ASP.NET Core MVC と WCF のベンチマークを比較したことがあって、 そのときは WCF の方がかなり速いなぁ、という結果だった。 ASP.NET Core MVC が思いのほか遅いのが気になったけど、それに関しては当時調べず。
このときのベンチマークで ASP.NET Core MVC が遅かった原因が、コンソールにログを出力していたせいだと判明。試しに Information 以下のログを出力しないように構成したら、だいぶ結果が変わったので、ベンチマークを比較し直すことにした。
ベンチマーク結果
ただやり直すだけでは面白くないので、ASP.NET Core MVC の方はシリアライザに ProtocolBuffers を使った場合のベンチマークも追加してみた。
ProtocolBuffers を使っても、NetTcpBinding を使った WCF には敵わなかったのが残念。
ベンチマークに使ったコード
ASP.NET Core MVC
WebApiContrib.Core.Formatter.Protobuf を導入することで、シリアライザに ProtocolBuffers が使えるようになる。
using System; using System.Linq; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ProtoBuf; using WebApiContrib.Core.Formatter.Protobuf; namespace AspNetCoreVsWcf.AspNetCore { public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureLogging(logging => { logging.SetMinimumLevel(LogLevel.Warning); }) .UseUrls("http://localhost:8000"); } public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddProtobufFormatters(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMvc(); } } [ProtoContract] public class Book { [ProtoMember(1)] public string Id { get; set; } [ProtoMember(2)] public string Title { get; set; } [ProtoMember(3)] public string Description { get; set; } [ProtoMember(4)] public string Author { get; set; } [ProtoMember(5)] public int Price { get; set; } [ProtoMember(6)] public string PublishedAt { get; set; } } [Route("api/[controller]")] [ApiController] public class BooksController : ControllerBase { static readonly Book[] books; static BooksController() { books = Enumerable.Range(0, 50) .Select(i => new Book() { Id = Guid.NewGuid().ToString(), Title = $"Book{i}", Author = $"Author{i}", Description = $"Description{i}", PublishedAt = DateTime.Today.ToString(), Price = 2000, }) .ToArray(); } [HttpGet] public ActionResult<Book[]> Get() { return books; } } }
WCF
using System; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; namespace AspNetCoreVsWcf.Wcf { class Program { static void Main(string[] args) { var host = new ServiceHost(typeof(BookService)); host.AddServiceEndpoint( typeof(IBookService), new NetTcpBinding(), "net.tcp://localhost:8001/BookService"); host.Open(); Console.WriteLine("Enter で終了"); Console.ReadLine(); host.Close(); } } [DataContract] public class Book { [DataMember] public string Id { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string Description { get; set; } [DataMember] public string Author { get; set; } [DataMember] public int Price { get; set; } [DataMember] public string PublishedAt { get; set; } } [ServiceContract] public interface IBookService { [OperationContract] Book[] GetBooks(); } public class BookService : IBookService { static readonly Book[] books; static BookService() { books = Enumerable.Range(0, 50) .Select(i => new Book() { Id = Guid.NewGuid().ToString(), Title = $"Book{i}", Author = $"Author{i}", Description = $"Description{i}", PublishedAt = DateTime.Today.ToString(), Price = 2000, }) .ToArray(); } public Book[] GetBooks() { return books; } } }
ベンチマーク
using System; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.Serialization; using System.ServiceModel; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using Newtonsoft.Json; using ProtoBuf; namespace AspNetCoreVsWcf.Benchmark { class Program { static void Main(string[] args) { BenchmarkRunner.Run<AspNetCoreVsWcfBenchmark>(); Console.ReadLine(); } } [ProtoContract] [DataContract] public class Book { [DataMember] [ProtoMember(1)] public string Id { get; set; } [DataMember] [ProtoMember(2)] public string Title { get; set; } [DataMember] [ProtoMember(3)] public string Description { get; set; } [DataMember] [ProtoMember(4)] public string Author { get; set; } [DataMember] [ProtoMember(5)] public int Price { get; set; } [DataMember] [ProtoMember(6)] public string PublishedAt { get; set; } } [ServiceContract] public interface IBookService { [OperationContract] Book[] GetBooks(); } public class AspNetCoreVsWcfBenchmark { IBookService tcpChannel; HttpClient httpClient; JsonSerializer jsonSerializer; [GlobalSetup] public void GlobalSetup() { tcpChannel = ChannelFactory<IBookService>.CreateChannel( new NetTcpBinding(), new EndpointAddress("net.tcp://localhost:8001/BookService")); httpClient = new HttpClient(); jsonSerializer = JsonSerializer.CreateDefault(); } [GlobalCleanup] public void GlobalCleanup() { ((IClientChannel)tcpChannel).Close(); httpClient.Dispose(); } [Benchmark] public Book[] WcfTcp() { return tcpChannel.GetBooks(); } [Benchmark] public async Task<Book[]> AspNetCoreJson() { var request = new HttpRequestMessage( HttpMethod.Get, "http://localhost:8000/api/books"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await httpClient.SendAsync(request); var stream = await response.Content.ReadAsStreamAsync(); using (var streamReader = new StreamReader(stream)) using (var jsonReader = new JsonTextReader(streamReader)) { return jsonSerializer.Deserialize<Book[]>(jsonReader); } } [Benchmark] public async Task<Book[]> AspNetCoreProtobuf() { var request = new HttpRequestMessage( HttpMethod.Get, "http://localhost:8000/api/books"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-protobuf")); var response = await httpClient.SendAsync(request); var stream = await response.Content.ReadAsStreamAsync(); return Serializer.Deserialize<Book[]>(stream); } } }