Skip to content
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

Fix #580 by using a fixed-point implementation for unit conversions using integer representations #615

Draft
wants to merge 39 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cf9c05f
wip
burnpanck Sep 15, 2024
3635260
disabled two tests which now trigger #614
burnpanck Sep 15, 2024
74f30da
again; fix C++20 compatibility
burnpanck Sep 15, 2024
2f54c76
addessed most review concerns, fixed CI failure
burnpanck Sep 16, 2024
e003a58
fixed and expanded double_width_int implemenation, tried to fix a bug…
burnpanck Sep 16, 2024
60c94da
one more try
burnpanck Sep 16, 2024
d00b330
fixed pedantic error
burnpanck Sep 16, 2024
99d4315
Merge remote-tracking branch 'upstream/master' into feature/fixed-poi…
burnpanck Nov 5, 2024
653d3d2
fix formatting issues
burnpanck Nov 5, 2024
ed2574f
allow use of __(u)int128, and always use std::bit_width and friends
burnpanck Nov 5, 2024
e688ffc
silence pedantic warning about __int128
burnpanck Nov 5, 2024
55d8fd6
cross-platform silencing of pedantic warning
burnpanck Nov 5, 2024
38dcf64
Merge remote-tracking branch 'upstream/master' into feature/fixed-poi…
burnpanck Nov 6, 2024
ad76149
Apply suggestions from code review
burnpanck Nov 6, 2024
95cc9f3
more review-requested changes, good test-coverage of double_width_int…
burnpanck Nov 6, 2024
5f8eb5c
made hi_ and lo_ private members of double_width_int
burnpanck Nov 6, 2024
1b57404
attempt to fix tests on apple clang
burnpanck Nov 6, 2024
f673619
try to work around issues around friend instantiations of double_widt…
burnpanck Nov 6, 2024
f642d37
fix: gcc-12 friend compilation issue workaround
mpusz Nov 9, 2024
b6a6752
implement dedicated facilities to customise scaling of numbers with m…
burnpanck Nov 10, 2024
647ce6b
fixed a few more details
burnpanck Nov 10, 2024
464ecd4
Merge remote-tracking branch 'upstream/master' into feature/fixed-poi…
burnpanck Nov 10, 2024
e933be7
fix a few issues uncovered in CI
burnpanck Nov 11, 2024
6873c8b
fix formatting
burnpanck Nov 11, 2024
65a0ee4
fix module exports - does not yet inlude other review input
burnpanck Nov 11, 2024
0c1971e
addressed most review input
burnpanck Nov 11, 2024
4ef0210
fix includes (and use curly braces for constructor calls in measurmen…
burnpanck Nov 12, 2024
35ed472
first attempt at generating sparse CI run matrix in python; also, can…
burnpanck Nov 12, 2024
329b9f5
Merge branch 'master' into feature/faster-CI
burnpanck Nov 12, 2024
7fa15d2
fix formatting
burnpanck Nov 12, 2024
e464677
don't test Clang 19 just yet; fix cancel-in-progres
burnpanck Nov 12, 2024
cc9ea9d
add cancel-in-progress to all workflows
burnpanck Nov 12, 2024
a51462c
missing checkout in generate-matrix step
burnpanck Nov 12, 2024
f4c8e90
fix boolean conan options in dynamic CI matrix
burnpanck Nov 12, 2024
01f44c6
heed github warning, and use output file instead of set-output comman…
burnpanck Nov 12, 2024
5713243
fix clang 16
burnpanck Nov 12, 2024
ff11878
exclude clang18+debug from freestanding again
burnpanck Nov 12, 2024
b35e241
fix clang on macos-14 (arm64)
burnpanck Nov 12, 2024
ef0e7b3
Merge branch 'feature/faster-CI' into feature/fixed-point-multiplicat…
burnpanck Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ endfunction()
add_mp_units_module(
core mp-units-core
HEADERS include/mp-units/bits/core_gmf.h
include/mp-units/bits/fixed_point.h
include/mp-units/bits/get_associated_quantity.h
include/mp-units/bits/hacks.h
include/mp-units/bits/math_concepts.h
Expand Down
308 changes: 308 additions & 0 deletions src/core/include/mp-units/bits/fixed_point.h
mpusz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
// The MIT License (MIT)
//
// Copyright (c) 2018 Mateusz Pusz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#pragma once

// IWYU pragma: private, include <mp-units/framework.h>
#include <mp-units/framework/magnitude.h>

#ifndef MP_UNITS_IN_MODULE_INTERFACE
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <bit>
#include <concepts>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <numbers>
#endif
#endif

namespace mp_units {
namespace detail {
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

// this class synthesizes a double-width integer from two base-width integers.
template<std::integral T>
struct double_width_int {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am not wrong, this class only works at compile-time. If that is the case, then all its interfaces should be consteval. If some older compilers will complain, then we have an MP_UNITS_CONSTEVAL workaround.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the arithmetic operators are needed at runtime for the fixed-point scaling (scaling an i64 with a constexpr fixed-point i64.64 is implemented as a multiplication by an i128 followed by a right-shift by 64 bit). There is a bit of freedom in what rounding behaviour we'd like to have, which would require support for some runtime addition/subtraction as-well.

On the other hand, constructors could be made consteval for the fixed-point use-case. That said, I was just increasing test coverage for the arithmetic operators, which I am currently writing as runtime tests. The goal is to test a significant number of value combinations, and while this could be done at compile-time, I fear for the compilation time :-).

static constexpr bool is_signed = std::is_signed_v<T>;
static constexpr std::size_t base_width = std::numeric_limits<std::make_unsigned_t<T>>::digits;
static constexpr std::size_t width = 2 * base_width;

using Th = T;
using Tl = std::make_unsigned_t<T>;

constexpr double_width_int() = default;
constexpr double_width_int(const double_width_int&) = default;

constexpr double_width_int& operator=(const double_width_int&) = default;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

constexpr double_width_int(Th hi_, Tl lo_) : hi(hi_), lo(lo_) {}

explicit(true) constexpr double_width_int(long double v)
burnpanck marked this conversation as resolved.
Show resolved Hide resolved
{
constexpr auto scale = int_power<long double>(2, base_width);
constexpr auto iscale = 1.l / scale;
hi = static_cast<Th>(v * iscale);
lo = static_cast<Tl>(v - (hi * scale));
}
template<std::integral U>
requires(is_signed || !std::is_signed_v<U>)
explicit(false) constexpr double_width_int(U v)
{
if constexpr (is_signed) {
hi = v < 0 ? Th{-1} : Th{0};
} else {
hi = 0;
}
lo = static_cast<Tl>(v);
}

explicit(true) constexpr operator Th() const { return static_cast<Th>(lo); }
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

constexpr auto operator<=>(const double_width_int&) const = default;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

// calculates the double-width product of two base-size integers; this implementation requires at least one of them to
// be unsigned
static constexpr double_width_int wide_product_of(Th lhs, Tl rhs)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a factory function? If so, shouldn't the constructors be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a factory function, but note that it is not equivalent to the constructor taking a (hi, lo) pair. wide_product_of(lhs,rhs) creates an instance representing lhs * rhs, while double_width_int(hi,lo) creates an instance representing (hi<<base_width) + lo.

One could argue though that there is a certain risk of confusion about the semantics of the two-argument constructor, so we may want to still make it private and instead expose a factory function from_nibbles or similar.

{
constexpr std::size_t half_width = base_width / 2;
constexpr Tl msk = (Tl(1) << half_width) - 1u;
Th l1 = lhs >> half_width;
Tl l0 = static_cast<Tl>(lhs) & msk;
Tl r1 = rhs >> half_width;
Tl r0 = rhs & msk;
Tl t00 = l0 * r0;
Tl t01 = l0 * r1;
Th t10 = l1 * static_cast<Th>(r0);
Th t11 = l1 * static_cast<Th>(r1);
Tl m = (t01 & msk) + (static_cast<Tl>(t10) & msk) + (t00 >> half_width);
Th o1 = t11 + static_cast<Th>(m >> half_width) + (t10 >> half_width) + static_cast<Th>(t01 >> half_width);
Tl o0 = (t00 & msk) | ((m & msk) << half_width);
return {o1, o0};
}

template<std::integral Rhs>
friend constexpr auto operator*(const double_width_int& lhs, Rhs rhs)
burnpanck marked this conversation as resolved.
Show resolved Hide resolved
{
// Normal C++ rules; with respect to signedness, the bigger type always wins.
using ret_t = double_width_int;
auto ret = ret_t::wide_product_of(rhs, lhs.lo);
ret.hi += lhs.hi * rhs;
return ret;
};
template<std::integral Lhs>
friend constexpr auto operator*(Lhs lhs, const double_width_int& rhs)
{
return rhs * lhs;
}
template<std::integral Rhs>
friend constexpr double_width_int operator/(const double_width_int& lhs, Rhs rhs)
{
// Normal C++ rules; with respect to signedness, the bigger type always wins.
using ret_t = double_width_int;
if constexpr (std::is_signed_v<Rhs>) {
if (rhs < 0) {
return (-lhs) / static_cast<Tl>(-rhs);
} else {
return lhs / static_cast<Tl>(rhs);
}
} else if constexpr (is_signed) {
if (lhs.hi < 0) {
return -((-lhs) / rhs);
} else {
using unsigned_t = double_width_int<Tl>;
auto tmp = unsigned_t{static_cast<Tl>(lhs.hi), lhs.lo} / rhs;
return ret_t{static_cast<Th>(tmp.hi), tmp.lo};
}
} else {
Th res_hi = lhs.hi / rhs;
// unfortunately, wide division is hard: https://en.wikipedia.org/wiki/Division_algorithm.
// Here, we just provide a somewhat naive implementation of long division.
Tl rem_hi = lhs.hi % rhs;
Tl rem_lo = lhs.lo;
Tl res_lo = 0;
for (std::size_t i = 0; i < base_width; ++i) {
// shift in one bit
rem_hi = (rem_hi << 1u) | (rem_lo >> (base_width - 1));
rem_lo <<= 1u;
res_lo <<= 1u;
// perform one bit of long division
if (rem_hi >= rhs) {
rem_hi -= rhs;
res_lo |= 1u;
}
}
return ret_t{res_hi, res_lo};
}
};

template<std::integral Rhs>
requires(std::numeric_limits<Rhs>::digits <= base_width)
friend constexpr double_width_int operator+(const double_width_int& lhs, Rhs rhs)
{
// this follows the usual (but somewhat dangerous) rules in C++; we "win", as we are the larger type.
// -> signed computation only of both types are signed
if constexpr (is_signed, std::is_signed_v<Rhs>) {
if (rhs < 0) return lhs - static_cast<std::make_unsigned_t<Rhs>>(-rhs);
}
Tl ret = lhs.lo + static_cast<Tl>(rhs);
return {lhs.hi + Th{ret < lhs.lo ? 1 : 0}, ret};
}
template<std::integral Lhs>
friend constexpr double_width_int operator+(Lhs lhs, const double_width_int& rhs)
{
return rhs + lhs;
}
template<std::integral Rhs>
requires(std::numeric_limits<Rhs>::digits <= base_width)
friend constexpr double_width_int operator-(const double_width_int& lhs, Rhs rhs)
{
// this follows the usual (but somewhat dangerous) rules in C++; we "win", as we are the larger type.
// -> signed computation only of both types are signed
if constexpr (is_signed, std::is_signed_v<Rhs>) {
if (rhs < 0) return lhs + static_cast<std::make_unsigned_t<Rhs>>(-rhs);
}
Tl ret = lhs.lo - static_cast<Tl>(rhs);
return {lhs.hi - Th{ret > lhs.lo ? 1 : 0}, ret};
}
template<std::integral Lhs>
friend constexpr double_width_int operator-(Lhs lhs, const double_width_int& rhs)
{
return rhs + lhs;
}

constexpr double_width_int operator-() const { return {(lo > 0 ? -1 : 0) - hi, -lo}; }

constexpr double_width_int operator>>(unsigned n) const
{
if (n >= base_width) {
return {static_cast<Th>(hi < 0 ? -1 : 0), static_cast<Tl>(hi >> (n - base_width))};
}
return {hi >> n, (static_cast<Tl>(hi) << (base_width - n)) | (lo >> n)};
}
constexpr double_width_int operator<<(unsigned n) const
{
if (n >= base_width) {
return {static_cast<Th>(lo << (n - base_width)), 0};
}
return {(hi << n) + static_cast<Th>(lo >> (base_width - n)), lo << n};
}

static constexpr double_width_int max() { return {std::numeric_limits<Th>::max(), std::numeric_limits<Tl>::max()}; }

Th hi;
Tl lo;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make those private? In case we do, can we name them with the _ postfix to be consistent with all other places in the library?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong feelings here. I was thinking it could be relevant for this type to be a literal type, given that we intend to use it mostly at compile-time. Perhaps it becomes useful in some numeric manipulation needed for magnitudes? Perhaps we want to store an offset between two quantity points/origins with 128 bits of precision? However, I'm aware that literal types are only needed if we want instances as NTTP, and neither of these use-cases definitely require a literal type. I'll make them private, we can still revert that later.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant structural type? litearal types are types that can be used in constexpr functions and those do not need public members.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, of course.

};

#if false && defined(__SIZEOF_INT128__)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, CI complains about the true-path because __int128_t is non-standard. Given that this is an implementation-detail, I should probably look for a way to disable that warning instead.

using int128_t = __int128;
using uint128_t = unsigned __int128;
inline constexpr std::size_t max_native_width = 128;
#else
using int128_t = double_width_int<std::int64_t>;
using uint128_t = double_width_int<std::uint64_t>;
constexpr std::size_t max_native_width = 64;
#endif

template<typename T>
constexpr std::size_t integer_rep_width_v = std::numeric_limits<std::make_unsigned_t<T>>::digits;
template<typename T>
constexpr std::size_t integer_rep_width_v<double_width_int<T>> = double_width_int<T>::width;

template<typename T>
constexpr bool is_signed_v = std::is_signed_v<T>;
template<typename T>
constexpr bool is_signed_v<double_width_int<T>> = double_width_int<T>::is_signed;

template<typename T>
using make_signed_t = std::make_signed_t<T>;

#if defined(__cpp_lib_int_pow2) && __cpp_lib_int_pow2 >= 202002L && false
template<std::size_t N>
using min_width_uint_t =
std::tuple_element_t<std::max<std::size_t>(4u, std::bit_width(N) + (std::has_single_bit(N) ? 0u : 1u)) - 4u,
std::tuple<std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t, uint128_t>>;
#else
template<std::size_t N>
using min_width_uint_t =
std::tuple_element_t<std::max<std::size_t>(
4u, std::numeric_limits<std::size_t>::digits - std::countl_zero(N) + (N & (N - 1) ? 1u : 0u)) -
4u,
std::tuple<std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t, uint128_t>>;
#endif

template<std::size_t N>
using min_width_int_t = make_signed_t<min_width_uint_t<N>>;

template<typename T>
using double_width_int_for_t = std::conditional_t<is_signed_v<T>, min_width_int_t<integer_rep_width_v<T> * 2>,
min_width_uint_t<integer_rep_width_v<T> * 2>>;

template<typename Lhs, typename Rhs>
constexpr auto wide_product_of(Lhs lhs, Rhs rhs)
{
if constexpr (integer_rep_width_v<Lhs> + integer_rep_width_v<Rhs> <= max_native_width) {
using T = std::common_type_t<double_width_int_for_t<Lhs>, double_width_int_for_t<Rhs>>;
return static_cast<T>(lhs) * static_cast<T>(rhs);
} else {
using T = double_width_int<std::common_type_t<Lhs, Rhs>>;
return T::wide_product_of(lhs, rhs);
}
}

// This class represents rational numbers using a fixed-point representation, with a symmetric number of digits (bits)
// on either side of the decimal point. The template argument `T` specifies the range of the integral part,
// thus this class uses twice as many bits as the provided type, but is able to precisely store exactly all integers
// from the declared type, as well as efficiently describe all rational factors that can be applied to that type
// and neither always cause underflow or overflow.
template<std::integral T>
struct fixed_point {
using repr_t = double_width_int_for_t<T>;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved
static constexpr std::size_t fractional_bits = integer_rep_width_v<T>;

constexpr fixed_point() = default;
constexpr fixed_point(const fixed_point&) = default;

constexpr fixed_point& operator=(const fixed_point&) = default;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

explicit constexpr fixed_point(repr_t v) : int_repr_is_an_implementation_detail_(v) {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use consteval here as well.

I understand that in other libraries this might be useful as a runtime-enabled type. However, this is my primary concern here. External libraries might start to depend on our implementation details because of this. This will make us harder to change the bits if needed without breaking user's code. We would be unable to remove it from the repo if we possibly find a better solution, or change its implementation (ABI breaks).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that is a valid concern. Users should rather use a dedicated fixed-point library such as CNL.

There is still the issue of testing though, I fear about the compilation time if we are going to test a significant number of values and value combinations.

Copy link
Owner

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 will be worse than the thousands of tests we have for quantities already. But if some members are needed at runtime, then it makes sense to keep this type of runtime-enabled.


explicit constexpr fixed_point(long double v) :
int_repr_is_an_implementation_detail_(static_cast<repr_t>(v * int_power<long double>(2, fractional_bits)))
{
}

template<std::integral U>
requires(integer_rep_width_v<U> <= integer_rep_width_v<T>)
constexpr auto scale(U v) const
{
auto res = v * int_repr_is_an_implementation_detail_;
return static_cast<std::conditional_t<is_signed_v<decltype((res))>, std::make_signed_t<U>, U>>(res >>
fractional_bits);
}

repr_t int_repr_is_an_implementation_detail_;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, literal type, but again, I don't actually need that right now. I'll make it private (... and remove the is_an_implementation_detail_). We can still change it if a need comes up.

};

} // namespace detail
} // namespace mp_units
Loading
Loading