public static T Add<T>(T left, T right) { return left + right; }People have come up with a large number of solutions, but they all have their own problems, ranging from messy looking syntax to less-than-optimal performance (particularly due to virtual function calls). The solution I propose is a somewhat hacky one (yes that's a word) which maintains both good performance and clean looking code: write the function in IL.
The code for the add method boils down to something like this:
ldarg.0 ldarg.1 add retFor those not familiar with IL, the above code adds the first to arguments of the method, then returns the result.
I found that this actually works with generic types. No funny errors, no nothing. Of course, I didn't actually compile any IL source code - I wrote a separate program to generate the assembly. The source code for it is this:
namespace Generator { using System; using System.Linq; using System.Reflection; using System.Reflection.Emit; public static class Program { public static void Main() { var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Operations"), AssemblyBuilderAccess.Save); var module = assemblyBuilder.DefineDynamicModule("Operations", "Operations.dll"); var type = module.DefineType("Operator", TypeAttributes.Public | TypeAttributes.Sealed); type.DefineBinaryOperatorMethod("Add", OpCodes.Add); type.DefineBinaryOperatorMethod("Subtract", OpCodes.Sub); type.DefineBinaryOperatorMethod("Multiply", OpCodes.Mul); type.DefineBinaryOperatorMethod("Divide", OpCodes.Div); type.DefineBinaryOperatorMethod("Remainder", OpCodes.Rem); type.CreateType(); assemblyBuilder.Save("Operations.dll"); } private static MethodBuilder DefineBinaryOperatorMethod(this TypeBuilder type, string name, OpCode operation) { var method = type.DefineMethod(name, MethodAttributes.Public | MethodAttributes.Static); var genericParameter = method.DefineGenericParameters("T").First(); genericParameter.SetBaseTypeConstraint(typeof(ValueType)); method.SetReturnType(genericParameter); method.SetParameters(genericParameter, genericParameter); method.DefineParameter(1, ParameterAttributes.None, "left"); method.DefineParameter(2, ParameterAttributes.None, "right"); var ilGenerator = method.GetILGenerator(); var continueLabel = ilGenerator.DefineLabel(); ilGenerator.Emit(OpCodes.Ldtoken, genericParameter); ilGenerator.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); ilGenerator.EmitCall(OpCodes.Callvirt, typeof(Type).GetProperty("IsPrimitive").GetGetMethod(), null); ilGenerator.Emit(OpCodes.Brtrue_S, continueLabel); ilGenerator.Emit(OpCodes.Ldstr, "The specified type is not supported by this operation."); ilGenerator.Emit(OpCodes.Newobj, typeof(ArgumentException).GetConstructor(new[] { typeof(string) })); ilGenerator.Emit(OpCodes.Throw); ilGenerator.MarkLabel(continueLabel); ilGenerator.Emit(OpCodes.Ldarg_0); ilGenerator.Emit(OpCodes.Ldarg_1); ilGenerator.Emit(operation); ilGenerator.Emit(OpCodes.Ret); return method; } } }In case your wondering what the ldtoken/call/callvirt/brtrue.s/ldstr/newobj/throw stuff is doing there, that just checks to make sure the type argument is a primitive type (yes this only works on primitive types), and throws an ArgumentException otherwise. Before adding that, .NET threw some pretty nasty exceptions when I tried it on some non-primitive types.
As a final note, the methods generated with indeed work on all primitive types. Even booleans. I was surprised to get a DivideByZeroException upon attempting this:
Operator.Divide(true, false);Again, this only works on primitive types. Remember, string and decimal are not primitive types. Perhaps this could be extended to calling the op_* functions...
After a couple small performance tests, I found that this method was nearly as fast as doing simple addition (x + y). Then again, I know nothing about microbenchmarks. Try it for yourself :D
Enjoy
This is a great function to use, it allows better design of generics.
ReplyDeleteI found that it is takes about 8 times longer to do the addition followed by subtraction operation.
I wonder if any of the overhead is due to calling the operator.dll? Could it be faster if every dll that uses the code contains the operator code directly? i.e. some kind of "in-line" function?
int steps = 100000000;
int A = 0;
Stopwatch sw = Stopwatch.StartNew();
for(int i = 0; i < steps; i++)
{
A = A + i;
A = i - A;
}
sw.Stop();
A = 0;
Stopwatch sw2 = Stopwatch.StartNew();
for (int i = 0; i < steps; i++ )
{
A = Operator.Add(i, A);
A = Operator.Subtract(A, i);
}
sw2.Stop();
Console.WriteLine("Normal operator = {0} milliseconds, result={1}", sw.ElapsedMilliseconds, A);
Console.WriteLine("Normal operator = {0} milliseconds, result={1}", sw2.ElapsedMilliseconds, A);
Console.WriteLine("Operator takes {0} times as long as ordinary method.", (double)sw2.ElapsedMilliseconds / (double)sw.ElapsedMilliseconds);
return;