-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Measure/document performance of interception #31
Comments
It's a great idea. I've not tried it before but maybe we could use BenchmarkDotNet. Scott Hanselman blogged about it a couple of years ago. Good point on the need to highlight that there's some use of |
I’m using BenchmarkDotNet for another project I’m working on, I’d be happy to look here just opens this to track the work :) |
As of the latest official release. Worth noting that Ratio is not important here, it should be a static cost (we don't do more work in interception when you do more work inside your methods :) ) BenchmarkDotNet=v0.11.5, OS=
Intel Core i7-6820HQ CPU 2.70GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
[Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3362.0
ShortRun : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3362.0
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
using System;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using Castle.DynamicProxy;
namespace ConsoleApp1
{
[RankColumn]
[MemoryDiagnoser]
public class Foo
{
public interface IRet
{
int RetVal();
}
private sealed class StaticRet : IRet
{
public int RetVal()
{
return 1;
}
}
private sealed class NoopAsyncInterceptor : AsyncInterceptorBase
{
protected override Task InterceptAsync(IInvocation invocation, Func<IInvocation, Task> proceed)
{
return proceed(invocation);
}
protected override Task<TResult> InterceptAsync<TResult>(IInvocation invocation, Func<IInvocation, Task<TResult>> proceed)
{
return proceed(invocation);
}
}
private IRet unwrapped;
private IRet wrapped;
[GlobalSetup]
public void Setup()
{
unwrapped = new StaticRet();
wrapped = new ProxyGenerator()
.CreateInterfaceProxyWithTargetInterface<IRet>(
unwrapped,
new NoopAsyncInterceptor());
}
[Benchmark(Baseline = true)]
public int Unwrapped()
{
return unwrapped.RetVal();
}
[Benchmark]
public int Wrapped()
{
return wrapped.RetVal();
}
}
internal static class Program
{
private static void Main()
{
BenchmarkRunner.Run<Foo>(
DefaultConfig.Instance.With(Job.ShortRun));
}
}
} |
Remove
|
Thanks for measuring but you actually tested Castle proxy itself. It is more interesting to see the difference between IInterceptor and IAsyncInterceptor as the last one adds more work and uses reflection (afaiu) in some places, especially for generic tasks |
For example this is what I have on my machine without special preporations (many apps are open etc):
[RankColumn]
[MemoryDiagnoser]
public class Foo
{
public interface IRet
{
int RetVal();
}
private sealed class StaticRet : IRet
{
public int RetVal()
{
return 1;
}
}
private sealed class NoopInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
invocation.Proceed();
}
}
private sealed class NoopAsyncInterceptor : AsyncInterceptorBase
{
protected override Task InterceptAsync(IInvocation invocation, Func<IInvocation, Task> proceed)
{
return proceed(invocation);
}
protected override Task<TResult> InterceptAsync<TResult>(IInvocation invocation, Func<IInvocation, Task<TResult>> proceed)
{
return proceed(invocation);
}
}
private IRet _wrappedWithCastleInterceptor;
private IRet _wrappedWithAsyncInterceptor;
[GlobalSetup]
public void Setup()
{
var unwrapped = new StaticRet();
_wrappedWithCastleInterceptor = new ProxyGenerator()
.CreateInterfaceProxyWithTargetInterface<IRet>(
unwrapped,
new NoopInterceptor());
_wrappedWithAsyncInterceptor = new ProxyGenerator()
.CreateInterfaceProxyWithTargetInterface<IRet>(
unwrapped,
new NoopAsyncInterceptor());
}
[Benchmark(Baseline = true)]
public int WrappedWithCastleInterceptor()
{
return _wrappedWithCastleInterceptor.RetVal();
}
[Benchmark]
public int WrappedWithAsyncInterceptor()
{
return _wrappedWithAsyncInterceptor.RetVal();
}
}
internal static class Program
{
private static void Main()
{
BenchmarkRunner.Run<Foo>(DefaultConfig.Instance);
}
} |
Yep, as expected:
public interface IRet
{
Task<int> RetVal();
}
private sealed class StaticRet : IRet
{
public Task<int> RetVal()
{
return Task.FromResult(1);
}
} |
I played a bit with possible ways for async interceptions and realized that AsyncInterceptor is pretty good. private sealed class MyNoopAsyncInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
invocation.Proceed();
if (invocation.ReturnValue is Task task) invocation.ReturnValue = NewTask(task);
async Task NewTask(Task t)
{
await t;
}
}
}
It seems the main difference is additional await operator in the chain. It affects the values so much just because BenchmarkDotNet performs the benchmark multiple times, so it is like private sealed class MyNoopAsyncInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
invocation.Proceed();
if (invocation.ReturnValue is Task task) invocation.ReturnValue = NewTask(task);
Task NewTask(Task t)
{
return t;
}
}
} Eventually, I want to thank you guys for the good lib. |
For uses such as implementing AOP in C#, performance of the library is a major concern. Ideally, it should be possible to use the library in production code (hot path even), but regardless we should tell users what the performance characteristics of the library are (e.g. if every call that is intercepted will hold a thread for the Wait()).
The text was updated successfully, but these errors were encountered: