You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Proposal: Alternative nuget with decimal as internal representation #285
New nugets: UnitsNet.Decimal, UnitsNet.Decimal.Signed, UnitsNet.Serialization.JsonNet.Decimal, UnitsNet.Serialization.JsonNet.Decimal.Signed
Change internal representation from double to decimal
Why
Decimal has some benefits, such as fixed precision (predictable rounding) and high precision.
However, duplicating all types would nearly double the nuget size, so we propose to create a new nuget using decimal as internal representation as well as the output type.
Example:
double meters = Length.FromMeters(10).Meters; // currently
decimal meters = Length.FromMeters(10).Meters; // new nuget
We could go this route, but I don't want to force usage to include the <double> part all over the place. Unfortunately there are many cases when it can't infer the type. Often when numbers are coded, they are integers and that would have to be explicitly specified as 1d or 1m. Some people don't even know about that possibility, and will use <double> to work around it instead and get a poor first impression. The nullable overloads are also particularly bad at type inference.
Even if we accepted this, it is not trivial to work with generic types internally. You can't readily multiply or add two generic numbers together. You either need to check the type and handle the calculation separately (which does not fix the size problem), or use some arithmetic abstraction code such as this. But.. it adds overhead and feels complicated. I'm not a fan, but it might be a way do it and maybe it works better than my prejudice tells me.
I've used Math.NET and it has the same thing with matrices, that can be either double, decimal, long and possibly more. They solved it by having both a Matrix<double> type as well as a separate "double" namespace with Matrix, Vector and other types written assuming double.
It worked quite well, but the same approach would cost us almost N times the nuget size.
So.. the way I see it we have these options:
Two nugets - but this causes its own problems, such as when people depend on library A that depends on UnitsNetDouble and they also depend on library B that depends on UnitsNetDecimal.
One nuget - twice the fun, if you think bytes are fun
Also, there is a 4th option to trim down the size. If possible, and I'm not yet sure if it is, we could split the nuget into a core/main nuget following the 80/20 rule (the top most used units, that satisfy 80% of users while only including about 20% of the units) plus the core building blocks: UnitSystem for parsing/ToString of unit abbreviations and UnitConverter for dynamic conversion.
Then we add all the other units as extensible nugets, that add additional quantities and/or units on top; such as electrical engineering, chemistry and other domains.
I still don't know how we deal with units that overlap domains and how to build a truly extensible unit system, because the biggest win is strongly typed quantities/units and the only way I see we can achieve that is by using extension methods.
Finally, there are tons of units that are simply prefixes of a base unit. We could get rid of all those combinations by instead doing something like Mass m = 1.Mega().Tonne() or 1.Tonne(Prefix.Mega), but.. ugh... I really don't like that syntax and it's a lot less discoverable than having all the units listed up by intellisense, but it's one way to really cut down on size.
After some research and a few console application projects, I don't think it's possible to have a generics with clever arithmetic without a breaking code change that also has good performance. If the quantity files are generic, then the user would have to specify the generic type every time it's consumed. I originally tried to solve this by having all the quantities be generic, and then create concrete types with the default(double), and other(decimal, int, etc) generics defined in the type itself. However this isn't possible (to the best of my knowledge) with structs, only classes (by inheritance).
Thanks for looking into this and sharing the details. Yes, it is annoying that structs do not support inheritance. Do you see a way to accomplish your generic arithmetic if we changed to using classes instead? It could be a tradeoff to consider.
Yes I definitely think it's possible. I would have to create separate namespaces for each type, or name each concrete type differently (lengthDouble, lengthDecimal, etc).
The only downside to using classes here instead of structs that I see is that classes are a reference type, so there is another call in memory to get the location of the class on the heap, and memory locality might be poor. This would only be a problem for software that requires really optimized, efficient unit conversions. I don't think that will be an issue for the majority of programs that use this package.
We shouldn't take lightly on the performance and memory implications of moving from struct to class, but I agree we should definitely look into what benefits it gives us and what drawbacks it introduces. UnitsNet is not a high performance library, neither is it a high-precision library. In my mind, at least, it is a convenience library. I know @eriove has been using this on applications with memory and performance constraints and would probably have a lot to say on the case.
We've been using the quantities in UnitsNet instead of ordinary doubles in all our code. The difference to suing doubles is very small at the moment, and is out weighted by compile time dimension analysis and easier communication with our distributed team. References types just wouldn't work. I haven't done any measurements on decimal so I'm not sure how that would hold up.
To me having one library with decimal and one with double as a backing type seems like the best solution.
I think it's a little pointless to debate performance before the code is actually measured. I'll see what I can do to get the ball rolling and then collect data on speed and memory usage to see if it makes a meaningful difference.
Sorry for the radio silence. I've taken a first pass at it. It's definitely a little more complicated than I originally thought it would be without introducing a major version change. I've forked the repo and pushed those changes if anybody is curious. It's not ready yet, but it's looking promising so far.
Reacted by Erik Ovegård and Andreas Gullberg Larsen
Note that there has been some discussions in #372 on reducing code size, such as removing a LOT of seemingly redundant number extension methods - or moving them out of the main nuget altogether.
Also in #371 we discuss adding a quantity interface to serve as a base type, and since interfaces introduces boxing of structs you lose the (potential) performance benefit of structs in those scenarios where you need to work with the the interface instead of each specific quantity type.
I no longer think this is a good idea. Instead, it seems more likely that we change from struct to class and unlock the capabilities of generics and inheritance to support different numeric types without having to create N types. Closing this.
Activity
0xferit commentedon Nov 9, 2017
I think we can use generics to support decimal internal representation without doubling source-code size.
Perhaps with a usage like this:
double meters = Length<double>.FromMeters(10).Meters;// double internal representationdecimal meters = Length<decimal>.FromMeters(10).Meters;// decimal internal representationAnd actually, if I'm not mistaken, C# is smart enough to infer types so we can omit generic type arguments and use like this:
double meters = Length.FromMeters(10).Meters;// double internal representationdecimal meters = Length.FromMeters(10).Meters;// decimal internal representationangularsen commentedon Nov 9, 2017
We could go this route, but I don't want to force usage to include the
<double>part all over the place. Unfortunately there are many cases when it can't infer the type. Often when numbers are coded, they are integers and that would have to be explicitly specified as1dor1m. Some people don't even know about that possibility, and will use<double>to work around it instead and get a poor first impression. The nullable overloads are also particularly bad at type inference.Even if we accepted this, it is not trivial to work with generic types internally. You can't readily multiply or add two generic numbers together. You either need to check the type and handle the calculation separately (which does not fix the size problem), or use some arithmetic abstraction code such as this. But.. it adds overhead and feels complicated. I'm not a fan, but it might be a way do it and maybe it works better than my prejudice tells me.
I've used Math.NET and it has the same thing with matrices, that can be either double, decimal, long and possibly more. They solved it by having both a
Matrix<double>type as well as a separate "double" namespace withMatrix,Vectorand other types written assumingdouble.It worked quite well, but the same approach would cost us almost N times the nuget size.
So.. the way I see it we have these options:
angularsen commentedon Nov 9, 2017
Also, there is a 4th option to trim down the size. If possible, and I'm not yet sure if it is, we could split the nuget into a core/main nuget following the 80/20 rule (the top most used units, that satisfy 80% of users while only including about 20% of the units) plus the core building blocks:
UnitSystemfor parsing/ToString of unit abbreviations andUnitConverterfor dynamic conversion.Then we add all the other units as extensible nugets, that add additional quantities and/or units on top; such as electrical engineering, chemistry and other domains.
I still don't know how we deal with units that overlap domains and how to build a truly extensible unit system, because the biggest win is strongly typed quantities/units and the only way I see we can achieve that is by using extension methods.
Finally, there are tons of units that are simply prefixes of a base unit. We could get rid of all those combinations by instead doing something like
Mass m = 1.Mega().Tonne()or1.Tonne(Prefix.Mega), but.. ugh... I really don't like that syntax and it's a lot less discoverable than having all the units listed up by intellisense, but it's one way to really cut down on size.TrevorVonSeggern commentedon Dec 14, 2017
After some research and a few console application projects, I don't think it's possible to have a generics with clever arithmetic without a breaking code change that also has good performance. If the quantity files are generic, then the user would have to specify the generic type every time it's consumed. I originally tried to solve this by having all the quantities be generic, and then create concrete types with the default(double), and other(decimal, int, etc) generics defined in the type itself. However this isn't possible (to the best of my knowledge) with structs, only classes (by inheritance).
TrevorVonSeggern commentedon Dec 14, 2017
I created an example project of how I used the generic arithmetic here. (based on the codeproject link above)
angularsen commentedon Dec 17, 2017
Thanks for looking into this and sharing the details. Yes, it is annoying that structs do not support inheritance. Do you see a way to accomplish your generic arithmetic if we changed to using classes instead? It could be a tradeoff to consider.
TrevorVonSeggern commentedon Dec 17, 2017
Yes I definitely think it's possible. I would have to create separate namespaces for each type, or name each concrete type differently (lengthDouble, lengthDecimal, etc).
The only downside to using classes here instead of structs that I see is that classes are a reference type, so there is another call in memory to get the location of the class on the heap, and memory locality might be poor. This would only be a problem for software that requires really optimized, efficient unit conversions. I don't think that will be an issue for the majority of programs that use this package.
0xferit commentedon Dec 18, 2017
Thanks for looking into this @TrevorVonSeggern . I believe using classes for quantities and units will be helpful in this and various other issues.
Therefore I'll cast my vote for generics with classes.
angularsen commentedon Dec 18, 2017
We shouldn't take lightly on the performance and memory implications of moving from struct to class, but I agree we should definitely look into what benefits it gives us and what drawbacks it introduces. UnitsNet is not a high performance library, neither is it a high-precision library. In my mind, at least, it is a convenience library. I know @eriove has been using this on applications with memory and performance constraints and would probably have a lot to say on the case.
eriove commentedon Dec 18, 2017
We've been using the quantities in UnitsNet instead of ordinary doubles in all our code. The difference to suing doubles is very small at the moment, and is out weighted by compile time dimension analysis and easier communication with our distributed team. References types just wouldn't work. I haven't done any measurements on decimal so I'm not sure how that would hold up.
To me having one library with decimal and one with double as a backing type seems like the best solution.
TrevorVonSeggern commentedon Dec 18, 2017
I think it's a little pointless to debate performance before the code is actually measured. I'll see what I can do to get the ball rolling and then collect data on speed and memory usage to see if it makes a meaningful difference.
json: Serialize constructed Value and Unit
json: Serialize constructed Value and Unit
json: Serialize constructed Value and Unit
json: Serialize constructed Value and Unit
TrevorVonSeggern commentedon Feb 13, 2018
Sorry for the radio silence. I've taken a first pass at it. It's definitely a little more complicated than I originally thought it would be without introducing a major version change. I've forked the repo and pushed those changes if anybody is curious. It's not ready yet, but it's looking promising so far.
angularsen commentedon Feb 13, 2018
Note that there has been some discussions in #372 on reducing code size, such as removing a LOT of seemingly redundant number extension methods - or moving them out of the main nuget altogether.
Also in #371 we discuss adding a quantity interface to serve as a base type, and since interfaces introduces boxing of structs you lose the (potential) performance benefit of structs in those scenarios where you need to work with the the interface instead of each specific quantity type.
json: Serialize constructed Value and Unit
json: Serialize constructed Value and Unit
json: Serialize constructed Value and Unit
angularsen commentedon Jul 21, 2019
I no longer think this is a good idea. Instead, it seems more likely that we change from
structtoclassand unlock the capabilities of generics and inheritance to support different numeric types without having to create N types. Closing this.