Here's a relevant bit from Sam's blog that talks briefly about the caching policy.
http://blogs.msdn.com/samng/archive/2008/10/29/dynamic-in-c.aspx
The DLR checks a cache to see if the
given action has already been bound
against the current set of arguments.
So in our example, we would do a type
match based on 1, 2, and the runtime
type of d. If we have a cache hit,
then we return the cached result. If
we do not have a cache hit, then the
DLR checks to see if the receiver is
an IDynamicObject. These guys are
essentially objects which know how to
take care of their own binding, such
as COM IDispatch objects, real dynamic
objects such as Ruby or Python ones,
or some .NET object that implements
the IDynamicObject interface. If it is
any of these, then the DLR calls off
to the IDO and asks it to bind the
action.
Note that the result of invoking the
IDO to bind is an expression tree that
represents the result of the binding.
If it is not an IDO, then the DLR
calls into the language binder (in our
case, the C# runtime binder) to bind
the operation. The C# runtime binder
will bind the action, and will return
an expression tree representing the
result of the bind. Once step 2 or 3
have happened, the resulting
expression tree is merged into the
caching mechanism so that any
subsequent calls can run against the
cache instead of being rebound.
However, what Sam doesn't mention is exactly what the cache miss policy is. There are two main cache-miss policies: (1) trigger a cache miss when the argument types change, (2) trigger a cache miss when the argument identities change.
Obviously the former is far more performant; working out when we can cache based solely on type is tricky. A detailed exegesis of how all that logic works would take rather a long time; hopefully I or Chris or Sam will do a blog post on it one of these days.