Search code examples
c#performancerestgrpcbenchmarkdotnet

gRPC communication having WORSE performance than REST?


I've recently started working gRPC, and always heard about it being much faster than REST. So I created I created a benchmark project, because I wanted to know how much faster it actually was. Turns out that after several different approaches, REST was always slightly better than gRPC. I'm not experienced neither with benchmarking nor gRPC, so something must be wrong. My guess is that maybe my benchmarking setup is not right.

So my question is: what is wrong with my benchmarking? Or do the results actually make sense?

I have created a repository with the complete code here: https://github.com/henridd/RestVsGrpcBenchmark. For this question, I'll be talking about the BenchmarkWithSamePayload run.

These are the results of the last test.

// * Summary *

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores
.NET SDK 8.0.201
  [Host]     : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2


| Method        | Mean     | Error   | StdDev  |
|-------------- |---------:|--------:|--------:|
| BenchmarkGrpc | 147.2 us | 0.71 us | 0.59 us |
| BenchmarkRest | 111.6 us | 0.68 us | 0.90 us |

// * Hints *
Outliers
  BenchmarkWithSamePayload.BenchmarkGrpc: Default -> 2 outliers were removed, 3 outliers were detected (145.56 us, 150.79 us, 152.36 us)
  BenchmarkWithSamePayload.BenchmarkRest: Default -> 8 outliers were removed (118.18 us..126.86 us)

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 us   : 1 Microsecond (0.000001 sec)

You can find below the relevant code for this benchmark.

Grpc

Protos

rpc SayHello (HelloRequest) returns (HelloReply);
message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Client

public class Sender
{
    private GrpcChannel _grpcChannel;
    private Greeter.GreeterClient _greeter;
    private HelloRequest _defaultRequest;

    public Sender()
    {
        _grpcChannel = GrpcChannel.ForAddress("http://localhost:5264");
        _greeter = new Greeter.GreeterClient(_grpcChannel);
        _defaultRequest = new HelloRequest() { Name = "default" };
    }

    public async Task<string> PostDefault()
    {
        var reply = await _greeter.SayHelloAsync(_defaultRequest);

        return reply.Message;
    }
}

Service

Program

using GrpcService.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddGrpc();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<Service>();

app.Run();

Service

public class Service : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}

REST

Messages

public class HelloRequest
{
    public string Name { get; set; }
}

HelloResponse

public class HelloResponse
{
    public string Message { get; set; }
}

Client

public class Sender
{
    private HttpClient _httpClient;
    private HelloRequest _defaultRequest;

    public Sender()
    {
        _httpClient = new HttpClient();
        _defaultRequest = new HelloRequest() { Name = "default" };
    }


    public async Task<string> PostDefault()
    {
        var content = JsonContent.Create(_defaultRequest);
        var response = await _httpClient.PostAsync("http://localhost:5082/greet", content);
        var responseString = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<HelloResponse>(responseString)!.Message;
    }

}

Server

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var app = builder.Build();

app.MapPost("/greet", (HelloRequest request) =>
{
    return new HelloResponse() { Message = "Hello " + request.Name };
});

app.Run();

Benchmark

Program

var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run<BenchmarkWithSamePayload>();

BenchmarkWithSamePayload

public class BenchmarkWithSamePayload : BenchmarkBase
{
    [Benchmark]
    public async Task<string> BenchmarkGrpc()
    {
        return await _gRpcSender.PostDefault();
    }

    [Benchmark]
    public async Task<string> BenchmarkRest()
    {
        return await _restSender.PostDefault();
    }
}

BenchmarkBase

namespace BenchmarkRunner
{
    using gRpcSender = GrpcClient.Sender;
    using RestSender = RestClient.Sender;

    public abstract class BenchmarkBase
    {
        protected gRpcSender _gRpcSender;
        protected RestSender _restSender;

        [GlobalSetup]
        public void Setup()
        {
            _gRpcSender = new gRpcSender();
            _restSender = new RestSender();
        }
    }
}

BenchmarkConfig (Required due to antivirus)

public class BenchmarkConfig : ManualConfig
{
    public BenchmarkConfig()
    {
        AddJob(Job.MediumRun.WithToolchain(InProcessNoEmitToolchain.Instance));
    }
}

Solution

  • First of all using microbenchmarking tools for performance testing of web applications can be considered not ideal since web services usually designed to provide better throughput than latency (i.e. handle more load than faster handling a single request).

    Secondary (based on the code) you are performing testing in quite specific circumstances - you are testing "simple" responses on the same machine while one of the main goals of gRPC is to reduce size of message send over the network (and arguably better compression might require slower serialization/deserialization) which can be less of the factor in a single threaded test over TCP on the same machine for obvious reasons.

    Also the generated GRPC server is using Task's out of the box, so you have a small difference already present there - the REST service does not bother with Task.FromResult which should add some overhead too (though not that much noticable for you REST service).

    TL;DR

    Use some load testing tool (like NBomber for example) ideally over network with and check the difference in the load REST/GRPC services would be able to handle.