2

Closed

Improve performance of DbContext initialization

description

A few tests have indicated that there is a performance benefit obtained when the context is cached and re-used in multiple queries. Profiles indicate that the performance difference when the context is cached is mostly from the call to InitializeContext, which is slowed down in part by the call to LazyInternalConnection.Initialize.

Attached is a project which serves as a performance test of two scenarios: when we create a new context to serve each query request, and when we have one context per thread to serve all query requests for that thread. By running this test (fully ngen’ed machine, power setting = high performance, test bits built for release-any cpu) I experienced what I was expecting in my personal computer using a modified version of EF that is not affected by the performance issue described in workitem 814:

• When a new context is created per request I was getting close to 1215 queries per second, stable.

• When the test used one context per thread I was getting close to 1515 queries per second, stable.

That’s 25% increased throughput just by caching the context. This gap can be reduced by making the context initialization more nimble. Step one is to lower the cost of the calls to LazyInternalConnection.Initialize. It is currently spending 8.3% of all CPU (inclusive samples):

LazyInternalContext.InitializeContext (8.3%)
=> DefaultModelCacheKeyFactory.Create (8.3%)
==> LazyInternalContext.get_ProviderName (8.3%)
====> LazyInternalConnection.get_ProviderName (8.3%)
=====> LazyInternalConnection.Initialize (8.0%)
=====> InternalConnection.get_ProviderName (0.3%)

In this case it appears like the lazy internal context initializes a connection only to get the provider name during initialization. My two suggestions for us to lower this cost are:
  1. Given a context’s metadata is cached, could we also cache the value returned by InternalConnection.get_ProviderName so the cost of InitializeContext is lower?
  2. Could we also lower the cost of LazyInternalConnection.Initialize?
A closer look at LazyInternalConnection.Initialize tells us that:
LazyInternalConnection.Initialize (8.0%)
=> LocalDbConnectionFactory.CreateConnection (4.6%)
==> SqlConnectionFactory.CreateConnection (2.8%)
===> DbConectionStringBuilder.get_ConnectionString (1.3%)
=> InternalConnection.OnConnectionInitialized (3.0%)
==> DbConnectionStringBuilder.ToString (1.6%)
===> DbConectionStringBuilder.get_ConnectionString (1.5%)
==> InternalConnection.AddAppNameCookieToConnectionString (1.4%)

Pretty much all of the CPU used by LazyInternalConnection.Initialize is spent doing two things:
  1. Obtaining the connection string in order to create a connection.
  2. Once the connection is created, it gets the connection string again in order to append an App Name Cookie to it.
I think it’s reasonable to believe that the LazyInternalConnection.Initialize process can be sped up by making it faster to retrieve the connection string using a cache, and possibly by avoiding repeating the same string operations on the connection string once we obtained it the first time. I think this cache wouldn’t add much of a memory footprint since typically applications use one single connection string.

file attachments

Closed Mar 5, 2013 at 9:02 PM by DavidObando
Verified. The performance of having one context per request is now very close to that of having one context per thread.
In my machine I'm seeing a throughput of 4050 requests per second when I have a context per request versus 4600 requests per second when I have a context per thread. That means we are shy of a ~12% in tax cost for this pattern on a fully saturated CPU. That is a long way from the initial ~32% we were observing.
It's great to see this kind of improvement.

comments

RoMiller wrote Feb 28, 2013 at 10:21 PM

Be sure to also verify the fix for http://entityframework.codeplex.com/workitem/862 when verifying this fix

ajcvickers wrote Mar 4, 2013 at 6:18 PM

This bug became more about making initialization of DbContext instances faster such that the cost of short-lived context instances became less. Several changes went into this and the details of these changes can be found in the commit comments of the following checkins:

3affed9c72a2
1ef63e53714e
a5faddeca2be
e3193424a344
b8e56957c69f
f3ae7e9722df
139501a407de

I believe that most of the big opportunities for improvement have been covered--mostly relating to o-space loading. There are certainly some more improvements that could be made here, but in triage we decided that these should come post-EF6.