Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 2

Well is there really a "solution" at all in general?

This particular case I think I constrained enough that you


can claim an answer but does it generalize? Let's look at what I got first, the raw results are pretty easy
to understand.

The experiment I conducted was to run a fixed number of queries (5000 in this case) but to break them up
so that the compiled query was reused a decreasing amount. The first run is the "best" 1 batch of 5000
selects all using the compiled query. Then 2 batches of 2500, and so on down to 5000 batches of 1. As
a control I also run the uncompiled case at each step expecting of course that it makes no difference.
Note the output indicates we selected a total of 25000 rows of data -- that is 5 per select as expected.
Here are the raw results:

public class Feedback : System.MulticastDelegate {


// Constructor
public Feedback(Object target, Int32 methodPtr);

// Method with same prototype as specified by the source code


public void virtual Invoke(Object value, Int32 item, Int32 numItems);

And there you have it. Even at 2 uses the compiled query still wins but at 1 use it loses. In fact, the
magic number for this particular query is about 1.5 average uses to break even. But why? And how
might it change?

_target System.Object Refers to the object that should be


operated upon when the callback
method is called. This is used for
instance method callbacks

Understanding how to compare delegates for equality is important when you try to manipulate delegate
chains, as discussed in the next section.

Delegate Chains

By themselves, delegates are incredibly useful. But delegates also support chaining, which makes
them even more useful. In my last column, I mentioned that each MulticastDelegate object has a
private field called _prev. This field holds a reference to another MulticastDelegate object. That is,
every object of type MulticastDelegate (or any type derived from MulticastDelegate) has a reference
to another MulticastDelegate-derived object. This field allows delegate objects to be part of a linked-
list.
The Delegate class defines three static methods that you can use to manipulate a linked-list chain
of delegate objects:

class System.Delegate {
// Combines the chains represented by head & tail, head is returned
// (NOTE: head will be the last delegate called)
public static Delegate Combine(Delegate tail, Delegate head);

// Creates a chain represented by the array of delegates


// (NOTE: entry 0 is the head and will be the last delegate called)
public static Delegate Combine(Delegate[] delegateArray);

// Removes a delegate matching values Target/Method from the chain.


// The new head is returned and will be the last delegate called
public static Delegate Remove(Delegate source, Delegate value);
}

When you construct a new delegate object, the object’s _prev field is set to null indicating that
there are no other objects in the linked-list. To combine two delegates into a linked-list, you call one
of Delegate’s static Combine methods:

Feedback fb1 = new Feedback(FeedbackToConsole);


Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2);
// The left side of Figure 1 shows what the
// chain looks like after the previous code executes

because we were seeing cases where our caching assumptions were resulting in catastrophically bad
performance (Mid Life Crisis due to retained compiled regular expressions in our cache) -- I didn't want to
make that mistake again.

So that's roughly how we end up at our final design. Any Linq to SQL user can choose how much or how
little caching is done. They control the lifetime, they can choose an easy mechanism (e.g. stuff it in a
static variable forever) or a complicated recycling method depending on their needs. Usually the simple
choice is adequate. And they can easily choose which queries to compile and which to just run in the
usual manner.

Let's get back to the overhead of compiled queries. Besides the one-time cost of creating the delegate
there is also an little extra delegate indirection on each run of the query plus the more complicated thing
we have to do: since the compiled query can span DataContexts we have to make sure that the
DataContext we are being given in any particular execution of a compiled query is compatible with the
DataContext that was provided when the query was compiled the first time.

Other than that the code path is basically the same, which means you come out ahead pretty quickly.
This test case was, as usual, designed to magnify the typical overheads so we can observe them. The
result set is a small number of rows, it is always the same rows, the database is local, and the query itself
is a simple one. All the usual costs of doing a query have been minimized. In the wild you would expect
the query to be more complicated, the database to be remote, the actual data returned to be larger and
not always the same data. This of course both reduces the benefit of compilation in the first place but
also, as a consolation prize, reduces the marginal overhead.

In short, if you expect to reuse the query at all, there is no performance related reason not to compile it.

You might also like