Is a struct wrapping a primitive value type a zero cost abstraction in C#?
Asked Answered
K

2

8

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

Kakaaba answered 12/7, 2017 at 16:47 Comment(9)
Why don't you try doing some testing?Moldy
If you want to know which of two horses is faster, do you ask strangers on the internet, or do you race the horses? You want to know the cost of a proposed program, then run the program and soon you will know its costs!Damron
Leaving aside the question of performance, I would suggest that having types AngleDegrees, AngleRadians, AngleGradians and so on is a bad idea. Have one Angle type which has properties AsDegrees, AsRadians, and so on, and factories FromRadians, and so on. The implementation details of your type are just that: implementation details. By embedding the implementation detail in the name of the type you restrict your ability to innovate and lower the level of abstraction.Damron
I think this question is too broad. You're basically saying "think of every possible thing I could do with this type and show me the JIT compiler optimizes it the way I want". That's asking way too much for a single question.Kep
@EricLippert Actually, one Angle struct with properties AsDegrees/AsRadians and factory methods FromRadians is exactly the code I have right now. I chose Angle for simplicity, but sometimes converting between the spaces isn't so cheap, so it's useful to require the API user to be explicit, rather than do the work behind the scenes. Terrible example - Vector3Cartesian and Vector3PolarKakaaba
Well, declaring the property readonly is one way of possibly tripping over hidden costs. Take it away, Jon Skeet. (Note that this post is quite old, though, and pre-RyuJIT. It could do with some verification to see if this still happens. And in that vein, any question that asks "will the JIT do this or that" has to be taken with the extreme caveat "you really won't know until you test, and even if you do your results may be invalid tomorrow". The JIT compiler does not adhere to a formal spec guaranteeing stuff.)Eleusis
Well, if there is a performance difference in your examples, I will be very disappointed in Microsoft.Verona
@EricLippert - Strangers on the internet might know more about horse anatomy and might tell you WHY one of your horses is running faster.Bearing
I just want to say that the question is excellent and I've been looking for the topic of wrapping primitive values for type safety. I have indexes, IDs, IDs of entity inside another entity and so on, easy to pass a wrong int to a wrong function. I was actually looking for best practices in general, not just performance. Good to see other people are also doing that.Proboscidean
D
6

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.

Dram answered 12/5, 2018 at 21:23 Comment(0)
V
0

I found no significant performance difference running a billion trials of DoNaught in debug mode with optimizations turned on. Sometimes, double won, and sometimes, the wrapper won.

Verona answered 13/7, 2017 at 17:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.