Sometimes I want to add more typesafety around raw doubles. One idea that comes up a lot would be adding unit information with the types. For example,
struct AngleRadians {
public readonly double Value;
/* Constructor, casting operator to AngleDegrees, etc omitted for brevity... */
}
In the case like above, where there is only a single field, will the JIT be able to optimize away this abstraction in all cases? What situations, if any, will result in extra generated machine instructions compared to similar code using an unwrapped double?
Any mention of premature optimization will be downvoted. I'm interested in knowing the ground truth.
Edit: To narrow the scope of the question, here are a couple of scenarios of particular interest...
// 1. Is the value-copy constructor zero cost?
// Is...
var angleRadians = new AngleRadians(myDouble);
// The same as...
var myDouble2 = myDouble;
// 2. Is field access zero cost?
// Is...
var myDouble2 = angleRadians.Value;
// The same as...
var myDouble2 = myDouble;
// 3. Is function passing zero cost?
// Is calling...
static void DoNaught(AngleRadians angle){}
// The same as...
static void DoNaught(double angle){}
// (disregarding inlining reducing this to a noop
These are some of the things I can think of off the top of my head. Of course, an excellent language designer like @EricLippert will likely think of more scenarios. So, even if these typical use cases are zero-cost, I still think it would be good to know if there is any case where the JIT doesn't treat a struct holding one value, and the unwrapped value as equivalent, without listing each possible code snippet as it's own question
All wrapper classes can be function-called, which converts an arbitrary value to the primitive type that the class represents. This is a descriptive way of converting to primitive types and I recommend it.
In addition to wrapping a primitive value by new -invoking a wrapper class, we can also do so generically by function-calling Object (the class of most objects): With Object (), we can even create instances of BigInt and Symbol (even though those classes can’t be new -invoked):
The wrapper classes Boolean, Number, and String can be instantiated via new: That is, new String () wraps a primitive string and produces a wrapper object. The wrapper classes BigInt (ES2020) and Symbol (ES6) are relatively new and can’t be instantiated:
As an aside, if the argument of Object () is an object, it is simply returned without any changes: The generic way of unwrapping a wrapper object is method .valueOf (): > new String ('abc').valueOf () 'abc' > new Number (123).valueOf () 123
There can be some slight and observable differences because of ABI requirements. For instance for Windows x64, a struct-wrapped float or double will be passed to a callee via an integer register, while floats and doubles are passed via XMM registers (similarly for returns). At most 4 ints and 4 floats can be passed via registers.
The actual impact of this is very context dependent.
If you extend your example to pass a mixture of at least 5 integer and struct-or-double args, you will run out of integer arg registers faster in the struct wrapped double case, and calls and accesses to the trailing (non-register passed) args in the callee will be slightly slower. But the effect can be subtle as the first callee access will usually cache the result back in a register.
Likewise if you pass a mixture of at least 5 doubles and struct wrapped doubles you can fit more things in registers at a call than if you passed all args as doubles or all args as struct wrapped doubles. So there might be some small advantage to having some struct wrapped doubles and some non struct wrapped doubles.
So in isolation, the pure call overhead and raw access to args is lower if more args fit in registers, and that means struct wrapping some doubles helps if there are a number of other doubles, and not struct wrapping helps if there are a number of other integers.
But there are complications if either the caller and callee both computes with the values and also receives or passes them -- typically in those cases struct wrapping will end up being a bit slower as the values must be moved from an int register to the stack or (possibly) a float register.
Whether or not this cancels out the small potential gains at the calls depends on the relative balance of computation vs calls and how many args are passed and what types the args are, register pressure, etc.
ABIs that have HFA struct passing rules tend to be better insulated from this kind of thing, as they can pass struct wrapped floats in float registers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With