One problem with C# generics is the lack of member constraints. In other words, this is impossible:
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
ret
For 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
1

View comments

  1. This is a great function to use, it allows better design of generics.

    I 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;

    ReplyDelete
Well, I got tired of Blogger :D. New blog here.
While developing spectral, I discovered something peculiar.

Consider this sample pseudo C#/IL:

public static T Sqrt<T>(T value) { ldarg.0 conv.r8 call double System.Math.Sqrt(double) ret } You'd expect some sort of error, right? The return type is T, but the returned value is a double.
I decided to shove my findings in the last post into a library, dubbed Spectral.

Download the latest change set here.
One problem with C# generics is the lack of member constraints.
A very funny thing happened today.

I believe in some language or another (can't remember which), the Abs function is actually a member function - x.abs() instead of abs(x). It also means you can do something like -1.abs(). I wanted to try to emulate that sort of thing with extensions methods.
Something wonderfully terrible happened today: I got reviewed.

A negative review, yes. But also a very stupid one.

The reviewer complains that the s/he can't compile the source because it won't load in VS2010.

Keep in mind this is a somewhat old game that I made quite some time ago, using...
This has been done a thousand times, but here goes anyway...

Ok, sort of. After much experimenting, this is the simplest method I could come up with.

This method uses linear depth, and reconstructs the position in world space.
Say you have a simple class which implements the singleton pattern.

Now you want to create a class which inherits the singleton functionality.

So, you call Derived.GetInstance() and...
In an effort to emulate Reactive Programming, I have created this nifty little class. It's preliminary (created five minutes ago), but seems useful enough for now.
Every once in a while, I run into this sort of ugly situation:

public MyClass(GraphicsDevice graphicsDevice)

: this(graphicsDevice, graphicsDevice.Viewport.Width, graphicsDevice.Viewport.Height)

{ }

public MyClass(GraphicsDevice graphicsDevice, int width, int height)

{

if (graphicsDevice == nu
Here's a small class for building vertex and index data. I used this sort of thing a while ago when I was just starting on my planet renderer. I quickly found out that it, while putting everything into one nice vertex buffer and index buffer (saving me draw calls), became way to expensive to use when I started using much larger amounts of data. Nonetheless, it's still useful for making smaller things, such as boxes.
OK, I lied. I've been looking at LOD again, except this time around, I'm using triangles, not square patches. I've hacked out a couple useful methods, namely: Closest Point on Triangle Distance from Point to Triangle
Where did all the stuff go?

My posts were all over the place. I was also getting very lazy about posting new content.

Overall, the blog wasn't quite turning out how I wanted it to be...

So, I'm starting over.

I'll be laying off the procedurals for a while.
Blog Archive
Active Projects
Active Projects
Total Pageviews
Total Pageviews
10816
Loading