-
Notifications
You must be signed in to change notification settings - Fork 383
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
Comments
This is by design. Primarily because we currently have We can reconsider if we change to support any numeric type and that pretty much requires changing from @tmilnthorp I know you had great interest in moving to https://github.com/angularsen/UnitsNet/issues?utf8=%E2%9C%93&q=nan |
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? |
I'm with you on the drawbacks of going to However, it does add two major benefits; inheritance and generics. This will allow us to let the user choose whether to use I'm still kind of split on this, but I think I'm willing to give |
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. |
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 I did a quick naive test just now and my first stumbling block is instantiating the generic types, and it's not related to 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));
}
} |
@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. |
Interesting question on the subject : https://social.msdn.microsoft.com/Forums/en-US/d2aa3b06-c55b-4b22-a4ea-61c9d54db8b7/operators-in-generic-interface?forum=csharplanguage |
There is a new feature proposed for C# 9.0 currently called "Shapes" 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. |
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:
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}");
}
} |
@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. |
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);
};
} |
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 😆
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}");
}
} |
@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. |
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. |
@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. |
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? |
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# |
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. |
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
The text was updated successfully, but these errors were encountered: