Skip to content
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

Caching access to virtual properties #24775

Merged
merged 5 commits into from
Apr 28, 2021

Conversation

vonzshik
Copy link
Contributor

Reading from virtual properties in a cycle/sequentially is very slow because the compiler can't cache the returned value from the property (because there might be side effects). This pr helps to alleviate this problem for some of the properties (the worst offender is QueryContext.Context, which is called every time a new MaterializationContext is created, but changing this is not trivial). Below is the benchmark results of reading from fields and properties in a cycle of 10.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 5 2400G with Radeon Vega Graphics, 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=6.0.100-preview.3.21202.5
  [Host]     : .NET Core 6.0.0 (CoreCLR 6.0.21.20104, CoreFX 6.0.21.20104), X64 RyuJIT
  DefaultJob : .NET Core 6.0.0 (CoreCLR 6.0.21.20104, CoreFX 6.0.21.20104), X64 RyuJIT

Method Mean Error StdDev
Field 2.885 ns 0.0515 ns 0.0456 ns
Property 2.895 ns 0.0644 ns 0.0537 ns
VirtualProperty 17.204 ns 0.3340 ns 0.2961 ns
VirtualPropertyCached 4.847 ns 0.0433 ns 0.0384 ns
benchmark code
public class VirtualPropertyBenchmark
{
	public int ValueField;

	public int ValueProperty { get; set; }

	public virtual int ValuePropertyVirtual { get; set; }

	[GlobalSetup]
	public void Setup()
	{
		ValueField = 42;
		ValueProperty = 42;
		ValuePropertyVirtual = 42;
	}

	[Benchmark]
	public int Field()
	{
		var valueSum = 0;
		for (var i = 0; i < 10; i++)
		{
			valueSum += ValueField;
		}

		return valueSum;
	}

	[Benchmark]
	public int Property()
	{
		var valueSum = 0;
		for (var i = 0; i < 10; i++)
		{
			valueSum += ValueProperty;
		}

		return valueSum;
	}

	[Benchmark]
	public int VirtualProperty()
	{
		var valueSum = 0;
		for (var i = 0; i < 10; i++)
		{
			valueSum += ValuePropertyVirtual;
		}

		return valueSum;
	}

	[Benchmark]
	public int VirtualPropertyCached()
	{
		var valueSum = 0;
		var value = ValuePropertyVirtual;
		for (var i = 0; i < 10; i++)
		{
			valueSum += value;
		}

		return valueSum;
	}
}

On fortunes benchmark this change got me around 50 ms/minute.

cc @roji

@dnfadmin
Copy link

dnfadmin commented Apr 27, 2021

CLA assistant check
All CLA requirements met.

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit I'm surprised - this simple change seems to get us a 1% increase in RPS... Seems like we're at a point where virtual dispatch starts to make a difference?

We should do the same in SplitQueryingEnumerable, and possibly also look at FromSqlQueryingEnumerable (for DbDataReader) and Cosmos.

@@ -172,17 +172,18 @@ public bool MoveNext()

if (hasNext)
{
var resultContext = _resultCoordinator.ResultContext;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single ResultContext is read-only on SingleQueryResultCoordinator, we could cache it as a field on SingleQueryingEnumerable rather than doing it on each MoveNext, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just make our result coordinators sealed and non-virtual properties? They are public and overridable because of pub-ternal philosophy. It it is hurting perf then I don't think it is worth sticking to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we can go that way too. Though we're accessing some other virtual properties in MoveNext which we should probably cache (DbDataReader, CancellationToken), so we'd go down the caching route anyway...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make members on QueryContext and derived classes non-virtual too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndriySvyryd yeah.. I pushed a commit which at least caches QueryContext.Context so it doesn't get recalculated all the time.

@roji
Copy link
Member

roji commented Apr 27, 2021

Pushed a commit eliminating some more virtual lookups in the QueryingEnumerables.

@roji roji requested a review from smitpatel April 27, 2021 19:13
Copy link
Contributor

@smitpatel smitpatel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need of _resultContext. Make whole structure for co-ordinating results non-virtual or leave them I can take care of it in future.

_cancellationToken/_dbDataReader are fine.

@roji roji requested a review from smitpatel April 28, 2021 19:01
@roji roji requested a review from smitpatel April 28, 2021 19:13
@roji roji merged commit 6f7f730 into dotnet:main Apr 28, 2021
@roji
Copy link
Member

roji commented Apr 28, 2021

Thanks again for this @vonzshik!!

@vonzshik vonzshik deleted the virtual-property-caching branch April 29, 2021 08:57
roji pushed a commit to roji/efcore that referenced this pull request May 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants