8

After coming up against this problem myself in trying to implement a generic Vector2<int/float/double> in C#, I've done a bunch of investigation into this problem, also described in this question:

Less generic generics? A possible solution for arithmetic in C# generics

These links contain some more background information and fascinating solution approaches:

https://jonskeet.uk/csharp/miscutil/usage/genericoperators.html

http://www.codeproject.com/KB/cs/genericnumerics.aspx

Now that C# 4.0 is out with its new versatile dynamic type, my question for the brilliant SO community, is this: is it a tool that could be used perhaps to build performant, generic Vector/Matrix/etc. numeric types?

Clearly a Vector2 could be built by simply like so:

public struct Vector2
{
    public dynamic X;
    public dynamic Y;

    public Vector2(dynamic x, dynamic y)
    {
        this.X = x;
        this.Y = y;
    }

    public static Vector2 operator+(Vector2 a, Vector2 b)
    {
        return new Vector2(a.X + b.X, a.Y + b.Y);
    }
}

but with this approach we have no type constraint here, so you could make a Vector2(3, 12.4572). Is there a way that we could mix dynamic members with a type parameter Vector2<int> to perform our math operations as would be done with ints?

Perhaps some form of casting could be used to ensure this.X is a T, though I don't know how that would perform.

tukaef
  • 9,074
  • 4
  • 29
  • 45
Collin Arnold
  • 229
  • 1
  • 5

1 Answers1

7

Only you can tell if dynamic operator invocations will meet your performance requirements, but it's certainly possible to address some of your type-safety concerns with generics - there's no reason that everything has to be checked at run-time just because of one little dynamic call:

// Consider making this type immutable
public struct Vector2<T>
{
    public T X;
    public T Y;

    public Vector2(T x, T y)
    {
        this.X = x;
        this.Y = y;
    }

    //  The only dangerous operation on the type
    public static Vector2<T> operator +(Vector2<T> a, Vector2<T> b)
    {
        return new Vector2<T>((dynamic)a.X + b.X, (dynamic)a.Y + b.Y);
    }
}

Now, the only dangerous operation is to actually add 2 vectors of the same type (the addition operator needs to work as expected on the type-argument), but everything else is perfectly type-safe, as it should be. You can't do new Vector<int>("a", 5), add a Vector<int> and a Vector<string>, or assign the addition of two Vector<int>s to a Vector<string>. Note that none of these errors would have been caught at compile-time with your original solution.

Note that:

  1. There's nothing to stop you from using generics here but going down the compiling-an-expression-tree route for the addition instead of dynamic. Delegate invocations aren't free, but they should in theory be faster than the dynamic approach in this case - at the very least, you avoid boxing value-types. Only you can tell if they will be fast enough, though.

  2. In all cases, consider writing a static-constructor that validates that the type-argument in fact has a suitable addition operator, so that type-errors happen early on in the game.


EDIT (OP isn't satisfied with the performance of dynamic here):

The expression-tree approach would look something like:

public struct Vector2<T>
{
    private static readonly Func<T, T, T> Add;

    // Create and cache adder delegate in the static constructor.
    // Will throw a TypeInitializationException
    // if you can't add Ts or if T + T != T 
    static Vector2()
    {
        var firstOperand = Expression.Parameter(typeof(T), "x");
        var secondOperand = Expression.Parameter(typeof(T), "y");
        var body = Expression.Add(firstOperand, secondOperand);
        Add = Expression.Lambda<Func<T, T, T>>
              (body, firstOperand, secondOperand).Compile();
    }

    public T X;
    public T Y;

    public Vector2(T x, T y)
    {
        this.X = x;
        this.Y = y;
    }

    public static Vector2<T> operator +(Vector2<T> a, Vector2<T> b)
    {
        // Delegate invocation instead of dynamic operator invocation.
        return new Vector2<T>(Add(a.X, b.X), Add(a.Y, b.Y));
    }
}
Ani
  • 111,048
  • 26
  • 262
  • 307
  • Hah, that's exactly what I was looking for - what a simple, elegant solution! If my understanding of how dispatch handlers are cached with dynamics is correct, then I'd expect the performance of this to be near identical to the raw math itself, though I should profile that to confirm. Thanks! – Collin Arnold Feb 16 '11 at 16:47
  • @Ani Unfortunately, the perf isn't there, sadly. I ran 10 million iterations of code like this: `place1 = new Vec2i(-3, 13) + new Vec2i(-173, 48) * 3;` and `place2 = new Vec2(-3, 13) + new Vec2(-173, 48) * 3;` and the results came out thusly: `Vec2i : 89.0051 ms` `Vec2: 1479.0846 ms`. `dynamic` probably prevent inlining or something like that. I guess I'll have to stick with duplicating that code for each numeric type. – Collin Arnold Feb 17 '11 at 05:14
  • 1
    @Collin Arnold: Did you try the expression-tree approach? – Ani Feb 17 '11 at 05:17
  • @Ani Well, my apologies but I didn't understand that particular suggestion from your answer. Are you referring to hand-writing or generating IL in some fashion to use for such cases? – Collin Arnold Feb 17 '11 at 05:20
  • @Collin: Does my edit help? On my machine, the results for 100 million iterations are: Vec2i : 2.92 seconds, Expression Tree : 4.09 seconds, Dynamic : 34.48 seconds. So expression-trees seem to be roughly 8x faster than dynamic. – Ani Feb 17 '11 at 06:22
  • @Ani Ah wow, that's an awesome approach! I think that performance difference is acceptable for the flexibility it provides. Thanks for all the help! – Collin Arnold Feb 17 '11 at 06:41