Introduction
As R�diger Klaehn explains in his article, Using Generics for calculations, attempting to use the +
, -
, etc. operators with generic types doesn't work. Not directly, at least. The current literature has favored a rather convoluted approach, well-described in Klaehn's article.
However, there exists another approach, much cleaner, which runs on average at least as fast as the equivalent non-generic version. Following is a brief explanation of my solution.
Lightweight Code Generation
Lightweight Code Generation (LCG) is a new facility in .NET 2.0 which allows for the creation of method delegates at runtime. It's lighter-weight than the traditional runtime creation in that you don't have to create entire classes. IronPython is a notable example of LCG at work.
Klaehn's article cites a UseNet post by Daniel O'Connell, showing how one would call the addition operator using ILGenerator.Emit()
. However, other posters in the thread become concerned about inflexibility and aesthetics, while Klaehn himself expressing concern about the speed of invocation.
There is, indeed, a price to be paid for the use of late-bound methods. In .NET 1.x, the normal Invoke()
mechanisms were relatively slow and painful. However, in 2.0, delegates have been optimized such that, if called correctly, they can be nearly as fast as a normal call. In the July 2005 issue of MSDN, in fact, Joel Pobar discusses calling speed in his article, Reflection: Dodge Common Performance Pitfalls to Craft Speedy Applications.
The Code
My solution provides a BinaryOperator
delegate and a static GenericOperatorFactory
, both generic.
public delegate TResult BinaryOperator<TLeft, TRight,
TResult>(TLeft left, TRight right);
static class GenericOperatorFactory<TLeft, TRight, TResult, TOwner>
{
private static BinaryOperator<TLeft, TRight, TResult> add;
public static BinaryOperator<TLeft, TRight, TResult> Add
{
get { ... }
}
}
As you can see, the class defines an Add
property. The getter is defined as follows:
public static BinaryOperator<TLeft, TRight, TResult> Add
{
get
{
if (add == null)
{
Console.WriteLine(@
"Creating Add delegate for:
TLeft = {0}
TRight = {1}
TResult = {2}
TOwner = {3}",
typeof(TLeft),
typeof(TRight),
typeof(TResult),
typeof(TOwner)
);
DynamicMethod method =
new DynamicMethod(
"op_Addition"
+ ":" + typeof(TLeft).ToString()
+ ":" + typeof(TRight).ToString()
+ ":" + typeof(TResult).ToString()
+ ":" + typeof(TOwner).ToString(),
typeof(TLeft),
new Type[] {
typeof(TLeft),
typeof(TRight)
},
typeof(TOwner)
);
ILGenerator generator = method.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
if (typeof(TLeft).IsPrimitive)
{
generator.Emit(OpCodes.Add);
}
else
{
MethodInfo info = typeof(TLeft).GetMethod(
"op_Addition",
new Type[] {
typeof(TLeft),
typeof(TRight)
},
null
);
generator.EmitCall(OpCodes.Call, info, null);
}
generator.Emit(OpCodes.Ret);
Console.WriteLine("Method name = " + method.Name);
add = (BinaryOperator<TLeft, TRight, TResult>)
method.CreateDelegate(typeof(BinaryOperator<TLeft, TRight, TResult>));
}
return add;
}
}
The code generation is actually very simple. It's also generic: the types of the left and right sides of the operation, as well as the return type and owner of the method are all parameterized. Since the CLR creates separate specializations of this generic class as it's called at runtime, there's no need for a Hashtable
or the like to store different versions of the Add
delegate: the CLR will call the version of GenericOperatorFactory
specified by the type parameters supplied.
Usage
Usage is an important consideration. Obnoxious syntax will prevent use, and incorrect use will lead to performance problems. Here's what I've got:
public class Foo<T>
{
public T Value;
public Foo(T newValue)
{
this.Value = newValue;
}
private static BinaryOperator<T, T, T> addTT;
public static Foo<T> operator +(Foo<T> p1, Foo<T> p2)
{
if (addTT == null)
{
addTT = GenericOperatorFactory<T, T, T, Foo<T>>.Add;
}
return new Foo<T>(addTT(p1.Value, p2.Value));
}
}
If you've seen invocations of runtime-generated code before, you've probably seen some form of Delegate.Invoke(...)
, and noted it took quite a while to call the method (during testing, it was running about 130x slower than a non-generic equivalent). Note, however that I'm storing the generated delegate in a private delegate member and calling it normally. There would be, I imagine, one per permutation of types in the operator. Another option, though slower, would be to use the GenericOperatorFactory
's cached copy. Again, there's a tradeoff between elegance and speed.
The class can now be instantiated and used:
Foo<int> fooInt1 = new Foo<int>(1);
Foo<int> fooInt2 = new Foo<int>(2);
Foo<int> result = fooInt1 + fooInt2;
Foo<int> error1 = fooInt1 + 2;
Compare this syntax to that in Using Generics for calculations, where there are several different interfaces to use:
class Lists<T,C>
where T:new()
where C:ICalculator<T>,new()
{
public static T Sum(List<T> list)
{
Number<T,C> sum=new T();
for(int i=0;i<list.Count;i++)
sum+=list[i];
return sum;
}
}
Performance
I've included a small test program which compares:
- a tight
int + int
loop;
- a
FooInt + FooInt
loop, where FooInt
is a non-generic class defining an operator+(int, int)
overload;
- a
Foo<int> + Foo<int>
loop, using the Foo<T>
generic described above;
- a
Foo<int> - Foo<int>
loop.
On my system, I get fairly consistent results while running the test program:
int = 1783293664; delta: 156254
FooInt = 1783293664; delta: 1406286
Foo<int> add = 1783293664; delta: 1093778
Foo<int> subtract = -1783293664; delta: 1250032
ratio add vs int = 7
ratio add vs FooInt = 0.777777777777778
ratio sub vs add = 1.14285714285714
"delta" is the elapsed ticks from start to end of the loop.
int + int
is of course faster than Foo<int>
, but only by 7x. However, Foo<int>
is roughly 30% faster than FooInt
(1/0.77 ~ 1.3). Repeated execution shows some variation -- occasionally large -- but the values tend to live somewhere near what is shown here.
What surprised me at this point is that the Foo<int>
speeds are generally faster than FooInt
. I've examined the IL, and I don't see why this should be. Someone out there, please enlighten me on the subject.
The relative speed of this code has not been tested against Klaehn's.
Conclusion
I hope this code proves itself a useful basis for learning about Lightweight Code Generation, and removes some of the latent anxieties people seem to have regarding dynamic invocation and generic calculations in general. Included are addition and subtraction; the other operators are left as an exercise to the developer. Until and unless Microsoft creates a fast-tracked mechanism for overloading operators in generic classes, we're left with such work-arounds.