Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for non-number values for double #695

Closed
ebfortin opened this issue Aug 18, 2019 · 18 comments
Closed

Support for non-number values for double #695

ebfortin opened this issue Aug 18, 2019 · 18 comments

Comments

@ebfortin
Copy link
Contributor

ebfortin commented Aug 18, 2019

Is your feature request related to a problem? Please describe.
In the framework I'm working on and using UnistNet with, I came into a situation where I get Inifinity for a calculation. Infinity is something that is totally valid for what I'm doing. However, there is a systematic check of NaN and Inifinity in UnitsNet that makes these "values" impossible to use.

Describe the solution you'd like
I would like to have non-numbers values of double to be permitted. Or at least an option to have the check disabled.

Describe alternatives you've considered
Currently I'm using double.MaxValue for infinity and double? for situation where I have something not applicable, something I used to signal with NaN. It creates more code.

Additional context
N/A

@angularsen
Copy link
Owner

This is by design. Primarily because we currently have decimal in a handful of quantities and that number type does not support infinity nor NaN, so we want the behavior to be consistent for all quantities.

We can reconsider if we change to support any numeric type and that pretty much requires changing from struct to class first to support generics and avoid explosion of method overloads. I think we will get there someday, but no one is currently pushing for it.

@tmilnthorp I know you had great interest in moving to class, just wanted to let you know that I've started leaning towards that being a good idea since there are some significant gains there, I just don't have the time or interest to pursue it myself. Let me know if you want to start that work.

https://github.com/angularsen/UnitsNet/issues?utf8=%E2%9C%93&q=nan

@ebfortin
Copy link
Contributor Author

ebfortin commented Aug 18, 2019

I'm not sure going to class is a good idea. All these values are really value types. So they should, I think, stay as struct. If it means having the same architecture as today, I'm fine with it.

But tell me, why would we need to go with class if we want to use generics? I did't use generic much with struct so I don't know about the limitations. What are they in UnitsNet case?

@angularsen
Copy link
Owner

I'm with you on the drawbacks of going to class. Semantically changing from value types to reference types, and the whole "can be null" semantics is something I truly feel is a misfit for our quantity types and why I argued against it in the first place.

However, it does add two major benefits; inheritance and generics. This will allow us to let the user choose whether to use float, double or decimal as backing type without having 3x quantity types and methods. Binary size is arguably a concern to some people when we start to generate so much code like we do. It also solves the whole "some quantities using decimal internally, while most others use double".

I'm still kind of split on this, but I think I'm willing to give class at least a try and play with it for some time and see how it feels before deciding finally.

@ebfortin
Copy link
Contributor Author

What are the limitation for generics usage with structs? I can't find any reference that state hard limitations. Or maybe I just didn't a thorough enough search.

@angularsen
Copy link
Owner

Uhm, I can't seem to recall exactly the limitation we hit. I believe it had something to do with using generics to generalize arithmetic across any numeric type and for some reason that was not possible with struct. Or maybe I am mixing things up, it's been so long since we last discussed and I didn't do the research myself.

I did a quick naive test just now and my first stumbling block is instantiating the generic types, and it's not related to struct really. Am I thinking about it all wrong? Is there a better way to do this? Neither of the below two approaches seem appealing due to boxing or reflection overhead.

void Main()
{
	Foo<float>.AddWithObjectBoxing(new Foo<float>(1), new Foo<float>(2)).Value.Dump("AddWithObjecBoxing: This should be 3");
	Foo<float>.AddWithDynamic(new Foo<float>(1), new Foo<float>(2)).Value.Dump("AddWithDynamic: This should be 3");
}

public struct Foo<T> where T : struct
{
	public T Value;
	public Foo(T val)
	{
		Value = val;
	}
	
	public static Foo<T> AddWithObjectBoxing(Foo<T> a, Foo<T> b) {
		if (a.Value is float aFloatValue && b.Value is float bFloatValue)
			return new Foo<T>((T)(object)(aFloatValue + bFloatValue));
			
		throw new NotImplementedException();
	}

	public static Foo<T> AddWithDynamic(Foo<T> a, Foo<T> b)
	{
		return new Foo<T>(((dynamic)a.Value) + ((dynamic)b.Value));
	}
}

@angularsen
Copy link
Owner

@ZacharyPatten earlier brought to our attention how to do arithmetics with generics in a performant way using runtime compilation. It sounds super interesting and I'd look closer at how he did that and try to learn from him.

#666 (comment)

@ebfortin
Copy link
Contributor Author

@ZacharyPatten
Copy link

There is a new feature proposed for C# 9.0 currently called "Shapes"
dotnet/csharplang#110

That will allow for zero overhead generic mathematics. However, that is a long way off (and is only a candidate feature for C# 9.0 so it is not set in stone). Also, "Shapes" would not 100% obsolete runtime compilation. Using runtime compilation you can optimize code for specific types, but you lose that ability when you use Shapes (I can elaborate on this topic if you would like).

Interfacing is a bad solution. If you want to go interfacing rather than runtime compilation, you might as well wait until C# 9.0 so you can use Shapes.

Also... If done right, the runtime compilation code will be very similar to the Shapes code when C# 9.0 comes, so it will be an easy transition (mostly just adding in the shapes as generic constraints).

So I still recommend my comments from the other issue. Runtime compilation is currently the best option.

@tmilnthorp
Copy link
Collaborator

Sorry I'm a bit slow in replying. We have a new baby (1 week now!), so I've been busy :)

I thought there were more constraints on generic structs too, but perhaps not. Shapes is quite a ways away, so using late-binding as you showed is probably good for now (I assume that's what @ZacharyPatten means by runtime compilation). The results are very interesting. They are very close. Well, 2-3x slower yes, perhaps but that's nothing compared to the flexibility this could give us. The timing for 10,000,000 was:

Timing...
Quantity time: 00:00:00.0236035
QuantityT<double> time: 00:00:00.6048074
    public struct Quantity
    {
        public Quantity(double value)
        {
            Value = value;
        }

        public double Value
        {
            get;
            private set;
        }

        public static Quantity operator +(Quantity left, Quantity right)
        {
            return new Quantity(left.Value + right.Value);
        }
    }

    public struct QuantityT<T>
    {
        public QuantityT(T value)
        {
            Value = value;
        }

        public T Value
        {
            get;
            private set;
        }

        public static QuantityT<T> operator +(QuantityT<T> left, QuantityT<T> right)
        {
            dynamic lValue = left.Value;
            dynamic rValue = right.Value;
            return new QuantityT<T>(lValue + rValue);
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            const int n = 10000000;

            var quantityList = new List<Quantity>(n);
            var quantityTList = new List<QuantityT<double>>(n);

            var random = new Random(n);

            for (int i = 0; i < n; i++)
            {
                var value = random.Next() * (100 - 1) + 1;

                quantityList.Add(new Quantity(value));
                quantityTList.Add(new QuantityT<double>(value));
            }

            Console.WriteLine("Timing...");

            var stopwatch = new Stopwatch();

            stopwatch.Start();

            for (int i = 0; i < n; i+=2)
            {
                var result = quantityList[i] + quantityList[i + 1];
            }

            stopwatch.Stop();

            Console.WriteLine($"Quantity time: {stopwatch.Elapsed}");

            stopwatch.Restart();

            for (int i = 0; i < n; i += 2)
            {
                var result = quantityTList[i] + quantityTList[i + 1];
            }

            stopwatch.Stop();

            Console.WriteLine($"QuantityT<double> time: {stopwatch.Elapsed}");
        }
    }

@ZacharyPatten
Copy link

@tmilnthorp No no no... You do not want to use dynamic. Dynamic is very slow. You want to use runtime compilation with Linq expressions. They are much faster than dynamic.

@ZacharyPatten
Copy link

Here is an example of addition. This is faster than using dynamic.

public static T Add<T>(T a, T b)
{
    return AddImplementation<T>.Function(a, b);
}

internal static class AddImplementation<T>
{
    internal static Func<T, T, T> Function = (T a, T b) =>
    {
        ParameterExpression A = Expression.Parameter(typeof(T));
        ParameterExpression B = Expression.Parameter(typeof(T));
        Expression BODY = Expression.Add(A, B);
        Function = Expression.Lambda<Func<T, T, T>>(BODY, A, B).Compile();
        return Function(a, b);
    };
}

@tmilnthorp
Copy link
Collaborator

I think no sleep is messing with me :) @ZacharyPatten is right, it is slow using dynamic. It's not 2-3x, it's 20-30x! hah. Need some sleep to read numbers! 😴

Cool stuff @ZacharyPatten. I haven't played with these much, so it's interesting to learn! I think this is very usable. Using you example I got it down to REAL 2-3x 😆

Timing...
Quantity time: 00:00:00.0154745
QuantityT<double> time: 00:00:00.0353160
    public struct Quantity
    {
        public Quantity(double value)
        {
            Value = value;
        }

        public double Value
        {
            get;
            private set;
        }

        public static Quantity operator +(Quantity left, Quantity right)
        {
            return new Quantity(left.Value + right.Value);
        }
    }

    public struct QuantityT<T>
    {
        public QuantityT(T value)
        {
            Value = value;
        }

        public T Value
        {
            get;
            private set;
        }

        private static Func<T, T, T> AdditionExpression = GenerateExpression();

        private static Func<T, T, T> GenerateExpression()
        {
            var l = Expression.Parameter(typeof(T));
            var r = Expression.Parameter(typeof(T));
            var add = Expression.Add(l, r);
            return Expression.Lambda<Func<T, T, T>>(add, l, r).Compile();
        }

        public static QuantityT<T> operator +(QuantityT<T> left, QuantityT<T> right)
        {
            var value = AdditionExpression(left.Value, right.Value);
            return new QuantityT<T>(value);
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            const int n = 10000000;

            var quantityList = new List<Quantity>(n);
            var quantityTList = new List<QuantityT<double>>(n);

            var random = new Random(n);

            for (int i = 0; i < n; i++)
            {
                var value = random.Next() * (100 - 1) + 1;

                quantityList.Add(new Quantity(value));
                quantityTList.Add(new QuantityT<double>(value));
            }

            Console.WriteLine("Timing...");

            var stopwatch = new Stopwatch();

            stopwatch.Start();

            for (int i = 0; i < n; i += 2)
            {
                var result = quantityList[i] + quantityList[i + 1];
            }

            stopwatch.Stop();

            Console.WriteLine($"Quantity time: {stopwatch.Elapsed}");

            stopwatch.Restart();

            for (int i = 0; i < n; i += 2)
            {
                var result = quantityTList[i] + quantityTList[i + 1];
            }

            stopwatch.Stop();

            Console.WriteLine($"QuantityT<double> time: {stopwatch.Elapsed}");
        }
    }

@ZacharyPatten
Copy link

ZacharyPatten commented Aug 27, 2019

@tmilnthorp That is looking much better. :) I have more examples of measurement mathematics on my Towel project if you need more examples. I chose to store unit conversions in a 2D jagged array (multiplication table), so it is quite a different approach to how Units.Net currently works.

@ebfortin
Copy link
Contributor Author

The 2-3x penality on performance is probably because the compiler is able to inline the call to operator+ when it's a double. I wonder how we could help the compiler inline AdditionExpression. Or the JIT later at runtime. I wonder also if contraining T to a value type (struct) would help with code optimisation at compile time and runtime.

@ZacharyPatten
Copy link

ZacharyPatten commented Aug 31, 2019

@ebfortin There might be a little room for optimization, but that is about as good as we will be able to do until if/when "Shapes" are added (aka type classes) to C#. It is scheduled for C# 9. Shapes allow for zero overhead generic mathematics because it takes advantage of inlining that occurs on struct generics.

So the answer to your "I wonder if..." is Shapes in C# 9.

@ebfortin
Copy link
Contributor Author

I got some good results by adding MethodImpl(MethodImplOptions.AggressiveOptimization) to the operator method. About 30%. AggressiveInlining did no change.

I would want to look at the x86 code produced to compare. Anyone have a good extensions to do that?

@ZacharyPatten
Copy link

I just want to note that Shapes or "Type Classes" got pushed back till at least C# 10 (I was under the impression it would be in C# 9)... :( So it will take a long time for that to be a feature in C#

@stale
Copy link

stale bot commented Nov 17, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants