-
Notifications
You must be signed in to change notification settings - Fork 78
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
implement value_cast<ToQ> and value_cast<ToQP> #571
base: master
Are you sure you want to change the base?
Conversation
5b6df8b
to
3bd4f4d
Compare
BTW, instead of |
77113cb
to
c51baae
Compare
…rigin in value_cast, to prevent overflow in more cases.
Given that it is somewhat nontrivial to implement a correct general |
Oh, shall I try to sqash? |
…ude_type_impl when both types are the same
* using ToQP = quantity_point<mm, B, int>; | ||
* auto qp = value_cast<ToQP>(quantity_point{1.23 * m}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if the value_cast<Q>(qp)
is really needed? Anyway, this example needs to be fixed ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, this example needs to be fixed
Right, indeed obvious copy-paste error.
I am not sure if the
value_cast<Q>(qp)
is really needed?
value_cast<Q>(qp)
operations are convenient as part of implementations of value_cast<QP>(qp)
-like operations (see below). While my implementation of the latter tries to select the best possible implementation based on the provided types, an expert user may know more about the value range that is in fact being used in their case and instead want to implement a different order of operations. Then, they would also like to be able to use value_cast<Q>(qp)
. Finally, it provides some symmetry to value_cast<Q>(q)
. On the other hand, one could argue that value_cast<Q>(qp)
misleadingly suggests that the result type is going to be Q
, which of course it is not. However, that same argument could be made for the value_cast<Repr>(...)
overloads. Anyway, I don't have all too strong feelings here, we can also drop it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think it is needed and it may be misleading indeed.
value_cast<Q>(q)
makes sense as someone may have a typedef for quantity
and does not want to extract a unit and rep all the time for a cast. The same applies to quantity_point
.
On the other hand, value_cast<Q>(qp)
is strange, as one should not have a quantity
typedef for an abstraction modeled through a quantity_point
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Three use-cases for value_cast<Q>
:
- You want guarantee there is no inadverted change of
point_origin
when going fromFromQP
toToQP
typedefs or template arguments you may have. Writingvalue_cast<typename ToQP::quantity_type>(qp)
is much shorter thanquantity_point{value_cast<typename ToQP::quantity_type>(qp.quantity_from(FromQP::point_origin)), FromQP::point_origin}
and also safer - You know something about
FromQP
andToQP
that allows you to choose a good order of calculations among {change reference point, change unit, change representation}. Implementing that, you may get back to use-cast 1) (i.e. casting the rest, not touching thepoint_origin
). - You want to implement a generic
FromQP
toToQP
, like my newsudo_cast<ToQP>
implementation. As it turns out, my proposed "intelligent"sudo_cast<QP>
is in fact just two instances of case 2): Depending on the involved types, I need to choose a specific order.
In general, in embedded use cases, we typically care a lot about representation types and may want to control each aspect separately. I checked, I'm already using that value_cast<Q>(qp)
quite a number of times. Often, sensors would work with values referencing some quite arbitrary reference point; unless calculations interface "external" quantity_point
types, I try not to touch the reference point because of truncation issues, and just juggle the Q
part of the representation - even though the values do represent quantity points all the time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing value_cast(qp) is much shorter than quantity_point{value_cast(qp.quantity_from(FromQP::point_origin)), FromQP::point_origin} and also safer
Why not to write: quantity_point{ToQP::unit, typename ToQP::rep>(qp}
? I know that it is still spelling two things instead of one but I would like to avoid suggesting people that they should make aliases for quantity_point
and quantity
for the same abstraction to make it work. Not everyone will be smart enough or restricting themselves to just use typename QP::quantity_type
as you mentioned above.
But if you think it is the only way to make it work, then please commit it to the repo. I just don't know how to get feedback about it before putting this under standardization. We will not be able to easily verify how many people depend on this feature in their code. Probably for ISO I will not propose it in the first run, and add it later if people will request for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, there is elegance in the symmetry that value cast guarantees to only change the representation (with potential for rounding, underflow and overflow, but no fundamental change of meaning), and it takes basically whatever you throw (assuming it makes sense). For a q
, you can today either throw a Q
, a U
or a rep
, and it all makes sense. For a qp
, you can now throw a QP
, a U
and a rep
. Why not a Q
? The're all sensible and clear.
For standardisation, I think you can easily specify the U
, rep
and Q
overloads of value_cast(qp)
in terms of the QP
overload (or in terms of the corresponding value_cast(q)
overloads), so it adds very little complexity.
That said, I don't need it to make it work. The current implementation of value_cast<QP>
delegates to the new sudo_cast<QP>
, whose implementation currently only uses sudo_cast
s (and .point_for
, which I possibly should fix - re your other comment). So we absolutely don't need value_cast<Q>(qp)
in the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, let's try it then 😃
constexpr Magnitude auto c_mag = get_canonical_unit(qp_type::unit).mag / get_canonical_unit(ToQP::unit).mag; | ||
constexpr Magnitude auto num = detail::numerator(c_mag); | ||
constexpr Magnitude auto den = detail::denominator(c_mag); | ||
constexpr Magnitude auto irr = c_mag * (den / num); | ||
using c_rep_type = detail::maybe_common_type<typename ToQP::rep, typename qp_type::rep>; | ||
using c_mag_type = detail::common_magnitude_type<c_mag>; | ||
using multiplier_type = conditional< | ||
treat_as_floating_point<c_rep_type>, | ||
// ensure that the multiplier is also floating-point | ||
conditional<std::is_arithmetic_v<value_type_t<c_rep_type>>, | ||
// reuse user's type if possible | ||
std::common_type_t<c_mag_type, value_type_t<c_rep_type>>, std::common_type_t<c_mag_type, double>>, | ||
c_mag_type>; | ||
constexpr auto val = [](Magnitude auto m) { return get_value<multiplier_type>(m); }; | ||
if constexpr (val(num) * val(irr) > val(den)) { | ||
// original unit had a larger unit magnitude; if we first convert to the common representation but retain the | ||
// unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal | ||
// for the offset computation. | ||
return value_cast<typename ToQP::quantity_type>( | ||
value_cast<c_rep_type>(std::forward<QP>(qp)).point_for(ToQP::point_origin)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we reuse sude_cast
here? I wouldn't like to duplicate the implementation. Maybe sudo_cast
could benefit from new logic as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sudo_cast
is being used (indirectly), through the two value_cast
. The apparent duplication is just in order to determine the conversion factor to be applied to the numeric value. The runtime code in the body here does not apply that conversion factor at all; but it needs to know that factor is eventually going to make number larger or smaller. That is needed in turn to decide when to do that conversion: if the number gets larger, we want to postpone until after the offset adjustment, and if it gets smaller, we want to do it before the offset adjustment.
We could probably create a second overload of sudo_cast
which operates on quantity_point
(basically the content of this value_cast
, minus the constraint on matching quantity_specs, and ensuring that the quantity_spec is being casted correctly). Then, we could extract that calculation of the rescaling factor into a helper function and use it from both overloads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer that. I've already learned that sudo_cast
is really fragile, and any changes should be verified against the codegen in Compiler Explorer. I would not like to have nearly copy pasted implementation in a separate file because it would be hard to maintain and keep consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@burnpanck, I hope that you successfully delivered the upgraded project on time. Do you still work on this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refactor is done, but we had to postpone a few features, which we are still delivering as we speak. I do intend to finish this one tough, as our code is already heavily reliant on these value_cast
variants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On it. I'm currently refactoring the implementation of the new value_cast<QP>
into a new overload of sudo_cast
, sharing the multiplier calculation between the two sudo_cast
overloads as you prefer. While doing so, I noticed a few details on the existing sudo_cast<Q>
that appear disputable to me. I may open a separate issue to review that with you, but for now, I'll just follow by analogy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as the code sharing between the two sudo_cast
overloads, regarding
I would prefer that. I've already learned that sudo_cast is really fragile, and any changes should be verified against the codegen in Compiler Explorer. I would not like to have nearly copy pasted implementation in a separate file because it would be hard to maintain and keep consistent.
The shared code is purely compile-time; all of the runtime behaviour of sudo_cast<Q>
was already re-used within my previous value_cast<QP>
implementation through the call to value_cast<Q>
. Now, the new sudo_cast<QP>
will share the compile-time computation through a separate helper, and re-use the runtime code through a call to sudo_cast<Q>
. sudo_cast<Q>
will have most of it's compile-time guts ripped out into that shared helper, but it's runtime-path will remain untouched.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't mean the final result might be wrong. It never was and we unit test for it as well.
What I meant here is that the same conversion may compile into more assembly instructions than a similar operation on a fundamental type. I do not know how to unit test this, and I always discover an issue only while preparing some slides to my talks that prove that there is not overhead over using double
. Unfortunately, I often find this to not be true anymore in this case and have to fix the sudo
cast.
This is why I would like you to check the assembly before merging. Alternatively, we can merge it, and then verify in the Compiler Explorer and fix things if the codegen will be worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but I did not touch the original sudo_cast
implementation of that multiplication, and the sudo_cast<QP>
implementation does not do any unit conversion on it's own. It fully delegates to that to the existing sudo_cast<Q>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! This is why I asked to have one place where those things are implemented. Initially, you copy-pasted it to another file, which would make it much harder to maintain and ensure synergy and correctness.
|
||
Overloads are also provided for instances of `quantity_point`. Furthermore, in that case, there is | ||
an overload `value_cast<ToQP>(qp)`, which is roughly equivalent to | ||
`value_cast<typename ToQP::quantity_type>(qp).point_for(ToQP::point_origin)`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value_cast<typename ToQP::quantity_type>(qp)
is still a bit controversial 😉
And even if not, then we first have to describe what it does and how to use it (e.g., QP::quantity_type
) before we use it in the description of value_cast<ToQP>(qp)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let me do another take on the documentation, this one here is indeed a bit short.
(std::remove_reference_t<FromQP>::unit != ToQP::unit)) | ||
[[nodiscard]] constexpr QuantityPoint auto sudo_cast(FromQP&& qp) | ||
{ | ||
using qp_type = std::remove_reference_t<FromQP>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are assuming here an integral representation type, which often will not be the case as double
is the default in the library. In cases of a floating point representation type, we should calculate the entire conversion factor in one step and then do only one multiplication operation on the value to get the result. Otherwise, we will not generate assembly equivalent to operations on double
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If my previous comment was unclear, I will just note that the relative_point_origin
stores an offset, which has its own unit and representation as well. We could factor in this conversion here as well (instead of calling point_for()
as a separate step).
However, maybe it would be too complex to do it here anyway and the measurable performance gains would not be that big.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, those considerations about order of the conversion operations are most relevant for integral types, where both underflow and overflow are likely. It doesn't hurt for double either. However, I'm not sure what you mean by "calculate the entire conversion factor in one step". A change of point-origin requires an addition. A change in unit requires a multiplication. This implementation here does at most one multiplication and at most one addition in all cases. Here is how:
- The initial check for same point-origin directly delegates to the
sudo_cast<Q>
implementation to select the "best" way to do that multiplication, and avoids the addition - The two code-paths for a change in point origin go as follos:
- The first path for where the unit gets smaller and thus the number gets larger:
- cast to the intermediate representation type using
sudo_cast
(no-op in the case of floatingpoints). previously, that was avalue_cast
, but I figured that within asudo_cast
,value_cast
is off limit. - add/subtract the change in reference using
.point_for
; hopefully, the implementation of.point_for
is such that this is a single addition and no multiplication - scale the unit/number and
- cast the to the final representation type using
sudo_cast
.
- cast to the intermediate representation type using
- The second path for where the unit gets larger and thus the number will get smaller (or stay the same):
- cast to the intermediate representation type and
- scale the unit/number using
sudo_cast
. If the intermediate representation type is the same as the source erpresentation and the units remain the same, this is ano_op
. - add/subtract the change in reference using
.point_for
- cast to the final representation type using
sudo_cast
. No-op for floating-point types.
- The first path for where the unit gets smaller and thus the number gets larger:
So I believe this implementation is as efficient as it can be through a careful selection of types. While both code-paths invoke sudo_cast
twice, there is always one where the source and target units match and no multiplication is needed. For floating-point, there is also not cast, so that cast is a no-op. Furthermore, the implementation here does never do any arithmetic itself - it delegates the addition to .point_for
and the multiplication to sudo_cast<Q>
. Personally, I would even prefer to call value_cast
instead, because in each of the two code-paths, one of the casts is a pure representation cast, which would be more clear as value_cast<rep>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, now I understand you are indicating that .point_for
may potentially change the unit and representation type? In that case, you are of course right, we should replace the .point_for
call here with a dedicated implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, I'm not sure what you mean by "calculate the entire conversion factor in one step".
I mean something like this:
if constexpr (std::is_floating_point_v<multiplier_type>) {
// this results in great assembly
constexpr auto ratio = val(num) / val(den) * val(irr);
// use precalculated ratio...
}
else // ...
Precalculation of ratio
as a constexpr
variable ensures that the assembly will include only one multiplication by this constant. The else
branch typically results in more assembly instructions.
I just added a bit more detail to the documentation, opting for the analogy to the |
Sure, it is great that you have time to look into this in more detail. All of this is too much for me to address in detail at once by working alone. There are still plenty of things to do in the library. I am so happy when someone comes to help me. Thank you! |
Fixes #568