-
Notifications
You must be signed in to change notification settings - Fork 391
Proposal: Reducing size of library (WIP) #372
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
Comments
Regarding proposal 1: I think it will be too troublesome both for developers and users to have lots of packages. Plus as you said, where does a conversion belong to? Regarding proposal 2: I liked it, especially the resulting syntax. I don't find it awkward. Also, I wonder if we could develop an algorithm to make conversions without defining each valid conversion. |
Added proposal 3. |
#275 looks interesting... |
I just tried a Release build locally before and after removing all the number extensions code. The difference is a STAGGERING 475 vs 939 kB ! I think we have found our low hanging fruit to reduce code size. Besides adding this as a separate nuget, we can also consider using the |
Another option to moving number extension methods out is to drastically cut down on them. I count 10 extension methods per unit to cover
With only |
This may be crazy, but removing all of the properties would save a lot of space in the source files. Not sure about the binaries. The From and As methods are sufficient, no? It seems a bit redundant. |
I'm open to all suggestions, but I do personally like the properties as they take up a lot less characters in the consuming code. There is without doubt a lot of code that is syntactic sugar, or convenience, that we can consider removing without losing functionality. NumberToXXX extension methods for instance are not necessary at all, and could possibly be a separate nuget if we go with a lean core nuget and additional nugets to add things like number extensions. |
Indeed! To be honest I like the properties (although not thrilled about a property doing a calculation). They do make the code using Units.NET a lot prettier. But, it depends what priority reducing library size is. I personally think multiple packages are harder to find and use. And what's 1MB these days anyways? Or 10? |
Absolutely, it's all about perspective. Most use cases would shrug at 1MB, but on the other hand if I'm writing a mobile app and care about deploy size runtime performance, then these things suddenly do matter. I do know of people using it in memory and compute constrained environments. Hopefully we can cater to both worlds somehow. The readme does state this library is about convenience and not high precision nor high performance - so there is that. Regarding multiple nugets, the idea is to use a metapackage to link in the core and extra packages that form today's content with today's nuget name. So most users would get it all out of the box, but those who care about size could strip off certain parts of it to keep things lean. I've been thinking a lot about keeping some not-so-widely used quantities outside the core, but we found that conversions between quantities would then be pretty much impossible to implement the way it works today. So the alternative is then to move syntactic sugar like number extensions and getter properties out into separate nugets (would have to be extension methods and not gette properties anymore). Not crazy about the idea, but it would work and probably save off quite a bit of binary size. Then there is the proposal #2 syntax: It could possibly also handle more complex things like |
I just happened to check the .dll size not long ago and it was 1.3 MB :-| I'm not sure we did anything wrong either, except adding a bunch more units. Another thing I recently came upon, is that we have duplicated all our From factory methods to handle nullability, like |
I wish the following worked with Windows Runtime Component, otherwise I'd say it's a viable option. Perhaps it might not matter given the WRC separation efforts, but then the API would be inconsistent... Define a global prefixes class with const values: public static class Prefixes
{
public const double Exa = 1e18;
public const double Peta = 1e15;
public const double Tera = 1e12;
public const double Giga = 1e9;
public const double Mega = 1e6;
public const double Kilo = 1e3;
public const double Hecto = 1e2;
public const double Deca = 1e1;
public const double Deci = 1e-1;
public const double Centi = 1e-2;
public const double Milli = 1e-3;
public const double Micro = 1e-6;
public const double Nano = 1e-9;
public const double Pico = 1e-12;
public const double Femto = 1e-15;
public const double Atto = 1e-18;
} Then have a class per unit that defines the conversion functions along with a static property for each one. Let's look at Length (Length2 for ease of writing test code 😄) public class Length2
{
public double Value { get; }
public LengthUnit Unit { get; }
public Length2( double value, LengthUnit unit)
{
Value = value;
Unit = unit;
}
public abstract class LengthUnitClass
{
public abstract bool IsBase { get; }
public abstract LengthUnit Unit { get; }
public Length2 ToBase( Length2 value )
{
var inBase = ToBase( value.Value );
return new Length2( inBase, Length2.BaseUnit );
}
public Length2 FromBase( Length2 value )
{
if( value.Unit != Length2.BaseUnit )
throw new InvalidOperationException( "The given value is not in base units." );
var inTarget = FromBase( value.Value );
return new Length2( inTarget, this.Unit );
}
protected abstract double ToBase( double value );
protected abstract double FromBase( double value );
public static Length2 operator *( double left, LengthUnitClass right )
{
return new Length2( left, right.Unit );
}
}
public class MetersUnitClass : LengthUnitClass
{
public override bool IsBase => true;
public override LengthUnit Unit => LengthUnit.Meter;
protected override double ToBase( double value )
{
return value * 1.0; // Generated from json
}
protected override double FromBase( double value )
{
return value / 1.0; // Generated from json
}
}
public class YardsUnitClass : LengthUnitClass
{
public override bool IsBase => false;
public override LengthUnit Unit => LengthUnit.Yard;
protected override double ToBase( double value )
{
return value * 0.9144; // Generated from json
}
protected override double FromBase( double value )
{
return value / 0.9144; // Generated from json
}
}
public Length2 ToUnit( LengthUnit targetUnit )
{
var unitClass = GetUnitClass( this.Unit );
var targetUnitClass = GetUnitClass( targetUnit );
var inBaseUnits = unitClass.ToBase( this );
return targetUnitClass.FromBase( inBaseUnits );
}
public double ToDouble()
{
return Value;
}
public double ToDouble( LengthUnit targetUnit )
{
var inTargetUnits = ToUnit( targetUnit );
return inTargetUnits.Value;
}
private static LengthUnitClass GetUnitClass( LengthUnit unit )
{
switch( unit )
{
case LengthUnit.Meter: return Length2.Meters;
case LengthUnit.Yard: return Length2.Yards;
default:
throw new InvalidOperationException( "Invalid unit." );
}
}
public static Length2 operator *( Length2 left, double right )
{
return new Length2( left.Value * right, left.Unit );
}
public static LengthUnit BaseUnit => LengthUnit.Meter;
public static MetersUnitClass Meters => new MetersUnitClass();
public static YardsUnitClass Yards => new YardsUnitClass();
} Which would let you do the following: var l1 = 3.0 * Length2.Meters;
var l2 = 3.0 * Prefixes.Kilo * Length2.Meters;
var l3 = 3.0 * Length2.Meters * Prefixes.Kilo;
var inYards = l1.ToUnit( LengthUnit.Yard );
var inYardsDouble = l1.ToDouble( LengthUnit.Yard ); |
Interesting approach! It does require changing to |
I think it may actually be fine for struct. But yes, operator overloads are the blocking factor 😞 |
V4 branch already addresses a lot of this and reduced size down to about 40%. I'm closing this issue and we will instead revisit this topic with a new, fresh perspective later if we still want to reduce yet more. One of the topics we've discussed lately is removing the static factory methods ( |
But it WAS lightweight at some point 🙈😅 A few possibilities: Move from
|
NOTE: This is work in progress and early thoughts, but input is most welcome.
Motivation
UnitsNet
nuget is nearing the 1MB mark and this is probably a lot bigger than most consumers would expect, since most users only work with a small set of quantities and units.Proposal 1 - Core nuget + quantity nugets + meta nugets
This tries to achieve a kind of extension model, where new quantities are added via nugets - potentially 3rd party quantities.
UnitsNet.Core
(UnitSystem
,UnitConverter
,QuantityParser
,UnitFormatter
,QuantityValue
etc..)UnitsNet
nuget to a meta package that brings in the most common units (think 80/20, this is what we want most people to use)UnitsNet.Quantities.Length
,UnitsNet.Quantities.Mass
etc. (around 80 of these now), which has theLength
struct type (Length.g.cs
andLength.extra.cs
files) and any related extension methods (NumberToLengthExtensions
), with a dependency onUnitsNet.Core
UnitsNet.Meta.ElectricalEngineering
,UnitsNet.Meta.AudioEngineering
,UnitsNet.Meta.PetroleumEngineering
,UnitsNet.Meta.ChemicalEngineering
Concerns:
Force = Pressure * Area
, when the user may not have added all three nuget packages? Should there be a nuget package for the Force+Pressure+Area combination? Can we move operator overloads as well as interfaces and enums for all units to the core nuget, while keeping the quantity implementations and the bulk of the quantity code in separate nugets? Need to think of something clever here.Proposal 2 - Use extension methods and builder pattern to reduce member count per quantity
Instead of this:
We could do something like this:
The idea is to move all the prefix code (milli, kilo, mega etc) to a base type #371 for reuse across quantities. We can achieve this by the builder pattern described above using inheritance or extension methods.
A similar approach could be made with conversion properties:
Concerns:
1e3
etc. We have 3 logarithmic quantitiesAmplitudeRatio
,PowerRatio
andLevel
where the arithmetic is entirely different.Proposal 3 - Move number extension methods to separate nuget
I just realized we have a ton of number extension methods, and this part of the library is something I think many simply don't use and could make sense to make opt-in.
For every unit, we have an extension method like
Length l = 1.cm();
andMass m = 85.kg();
. To support the four number types (int
,long
,double
,decimal
) we currently care about, we have 4 overloads of this method. And to support nullable likeint? nullable = 10; Length? l = nullable.cm();
, we double that count. This means, for our 600+ units today there is now 8 x 600 extension methods. I would be very interested to see the diff in size when removing this, but I think it can be pretty significant since it seems it is mainly our high method/property count that causes the big size and not so much the amount of code inside each of them.Update 2018-01-20:
I just tried a Release build locally before and after removing all the number extensions code. The difference is a STAGGERING 939 vs 475 kB ! I think we have found our low hanging fruit to reduce code size.
Besides adding this as a separate nuget, we can also consider using the
QuantityValue
type to implicitly cast from multiple number types per extension method, to avoid the method overload explosion.The text was updated successfully, but these errors were encountered: