LibC: Add rounding specializations for RISC-V

Whenever the floating-point values are in integer range, we can use the
various FCVT functions with static rounding mode to perform fast
rounding. I took this opportunity to clean up the architecture
differentiation for these functions, which allows us to use the software
rounding implementation for all extreme and unimplemented cases,
including AArch64.

Also adds more round & trunc tests.
This commit is contained in:
kleines Filmröllchen 2024-01-06 19:08:39 +01:00 committed by Andrew Kaster
commit 0c1ad05f50
Notes: sideshowbarker 2024-07-17 10:05:47 +09:00
2 changed files with 301 additions and 208 deletions

View file

@ -56,12 +56,6 @@ TEST_CASE(trig)
EXPECT_APPROXIMATE(atan(555.5), 1.568996);
}
TEST_CASE(other)
{
EXPECT_EQ(trunc(9999999999999.5), 9999999999999.0);
EXPECT_EQ(trunc(-9999999999999.5), -9999999999999.0);
}
TEST_CASE(exponents)
{
struct values {
@ -272,22 +266,77 @@ TEST_CASE(acos)
TEST_CASE(floor)
{
EXPECT_EQ(floor(0.125), 0);
EXPECT_EQ(floor(-0.125), -1.0);
EXPECT_EQ(floor(0.5), 0);
EXPECT_EQ(floor(-0.5), -1.0);
EXPECT_EQ(floor(0.25), 0);
EXPECT_EQ(floor(-0.25), -1.0);
EXPECT_EQ(floor(-3.0 / 2.0), -2.0);
// NOTE: We run tests for all three float types since architecture-specific code may vary significantly between types.
#define TEST_FLOOR_FOR(suffix) \
EXPECT_EQ(floor##suffix(0.125f), 0.f); \
EXPECT_EQ(floor##suffix(-0.125f), -1.0f); \
EXPECT_EQ(floor##suffix(0.5f), 0.f); \
EXPECT_EQ(floor##suffix(-0.5f), -1.0f); \
EXPECT_EQ(floor##suffix(0.25f), 0.f); \
EXPECT_EQ(floor##suffix(-0.25f), -1.0f); \
EXPECT_EQ(floor##suffix(-3.0f / 2.0f), -2.0f);
TEST_FLOOR_FOR();
TEST_FLOOR_FOR(f);
TEST_FLOOR_FOR(l);
EXPECT_EQ(floor(-9999999999999.5), -10000000000000.0);
EXPECT_EQ(floor(9999999999999.5), 9999999999999.0);
}
TEST_CASE(ceil)
{
EXPECT_EQ(ceil(0.125), 1.0);
EXPECT_EQ(ceil(-0.125), 0);
EXPECT_EQ(ceil(0.5), 1.0);
EXPECT_EQ(ceil(-0.5), 0);
EXPECT_EQ(ceil(0.25), 1.0);
EXPECT_EQ(ceil(-0.25), 0);
EXPECT_EQ(ceil(-3.0 / 2.0), -1.0);
#define TEST_CEIL_FOR(suffix) \
EXPECT_EQ(ceil##suffix(0.125##suffix), 1.0##suffix); \
EXPECT_EQ(ceil##suffix(-0.125##suffix), 0.##suffix); \
EXPECT_EQ(ceil##suffix(0.5##suffix), 1.0##suffix); \
EXPECT_EQ(ceil##suffix(-0.5##suffix), 0.##suffix); \
EXPECT_EQ(ceil##suffix(0.25##suffix), 1.0##suffix); \
EXPECT_EQ(ceil##suffix(-0.25##suffix), 0.##suffix); \
EXPECT_EQ(ceil##suffix(-3.0##suffix / 2.0##suffix), -1.0##suffix);
TEST_CEIL_FOR();
TEST_CEIL_FOR(f);
TEST_CEIL_FOR(l);
EXPECT_EQ(ceil(9999999999999.5), 10000000000000.0);
EXPECT_EQ(ceil(-9999999999999.5), -9999999999999.0);
}
TEST_CASE(trunc)
{
#define TEST_TRUNC_FOR(suffix) \
EXPECT_EQ(trunc##suffix(0.125##suffix), 0.##suffix); \
EXPECT_EQ(trunc##suffix(-0.125##suffix), 0.##suffix); \
EXPECT_EQ(trunc##suffix(0.5##suffix), 0.##suffix); \
EXPECT_EQ(trunc##suffix(-0.5##suffix), 0.##suffix); \
EXPECT_EQ(trunc##suffix(0.25##suffix), 0.##suffix); \
EXPECT_EQ(trunc##suffix(-0.25##suffix), 0.##suffix); \
EXPECT_EQ(trunc##suffix(-3.0##suffix / 2.0##suffix), -1.0##suffix);
TEST_TRUNC_FOR();
TEST_TRUNC_FOR(f);
TEST_TRUNC_FOR(l);
EXPECT_EQ(trunc(9999999999999.5), 9999999999999.0);
EXPECT_EQ(trunc(-9999999999999.5), -9999999999999.0);
}
TEST_CASE(round)
{
#define TEST_ROUND_FOR(suffix) \
EXPECT_EQ(round##suffix(0.125##suffix), 0.##suffix); \
EXPECT_EQ(round##suffix(-0.125##suffix), 0.##suffix); \
EXPECT_EQ(round##suffix(0.5##suffix), 1.0##suffix); \
EXPECT_EQ(round##suffix(-0.5##suffix), -1.0##suffix); \
EXPECT_EQ(round##suffix(0.25##suffix), 0.##suffix); \
EXPECT_EQ(round##suffix(-0.25##suffix), 0.##suffix); \
EXPECT_EQ(round##suffix(-3.0##suffix / 2.0##suffix), -2.0##suffix);
TEST_ROUND_FOR();
TEST_ROUND_FOR(f);
TEST_ROUND_FOR(l);
EXPECT_EQ(round(9999999999999.5), 10000000000000.0);
EXPECT_EQ(round(-9999999999999.5), -10000000000000.0);
}

View file

@ -57,7 +57,9 @@ enum class RoundingMode {
ToZero = FE_TOWARDZERO,
Up = FE_UPWARD,
Down = FE_DOWNWARD,
ToEven = FE_TONEAREST
ToEven = FE_TONEAREST,
// Round to nearest, ties away from zero.
ToMaxMagnitude = FE_TOMAXMAGNITUDE,
};
// This is much branchier than it really needs to be
@ -118,6 +120,9 @@ static FloatType internal_to_integer(FloatType x, RoundingMode rounding_mode)
break;
case RoundingMode::ToZero:
break;
case RoundingMode::ToMaxMagnitude:
should_round = true;
break;
}
if (should_round) {
@ -397,6 +402,14 @@ double trunc(double x) NOEXCEPT
: [temp] "m"(temp));
return x;
}
#elif ARCH(RISCV64)
if (fabs(x) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.d %0, %1, rtz"
: "=r"(output)
: "f"(x));
return static_cast<double>(output);
}
#endif
return internal_to_integer(x, RoundingMode::ToZero);
@ -414,6 +427,14 @@ float truncf(float x) NOEXCEPT
: [temp] "m"(temp));
return x;
}
#elif ARCH(RISCV64)
if (fabsf(x) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.s %0, %1, rtz"
: "=r"(output)
: "f"(x));
return static_cast<float>(output);
}
#endif
return internal_to_integer(x, RoundingMode::ToZero);
@ -444,8 +465,12 @@ double rint(double value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
i64 output;
// FIXME: This saturates at 64-bit integer boundaries; see Table 11.4 (RISC-V Unprivileged ISA V20191213)
asm("fcvt.l.d %0, %1, dyn"
: "=r"(output)
: "f"(value));
return static_cast<double>(output);
#elif ARCH(X86_64)
double res;
asm(
@ -463,8 +488,12 @@ float rintf(float value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
i64 output;
// FIXME: This saturates at 64-bit integer boundaries; see Table 11.4 (RISC-V Unprivileged ISA V20191213)
asm("fcvt.l.s %0, %1, dyn"
: "=r"(output)
: "f"(value));
return static_cast<float>(output);
#elif ARCH(X86_64)
float res;
asm(
@ -503,8 +532,12 @@ long lrint(double value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
i64 output;
// FIXME: This saturates at 64-bit integer boundaries; see Table 11.4 (RISC-V Unprivileged ISA V20191213)
asm("fcvt.l.d %0, %1, dyn"
: "=r"(output)
: "f"(value));
return output;
#elif ARCH(X86_64)
long res;
asm(
@ -523,8 +556,12 @@ long lrintf(float value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
i64 output;
// FIXME: This saturates at 64-bit integer boundaries; see Table 11.4 (RISC-V Unprivileged ISA V20191213)
asm("fcvt.l.s %0, %1, dyn"
: "=r"(output)
: "f"(value));
return output;
#elif ARCH(X86_64)
long res;
asm(
@ -544,8 +581,8 @@ long long llrintl(long double value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
// NOTE: RISC-V LP64 specifies long long == long.
return static_cast<long long>(lrintl(value));
#elif ARCH(X86_64)
long long res;
asm(
@ -564,8 +601,8 @@ long long llrint(double value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
// NOTE: RISC-V LP64 specifies long long == long.
return static_cast<long long>(lrint(value));
#elif ARCH(X86_64)
long long res;
asm(
@ -584,8 +621,8 @@ long long llrintf(float value)
(void)value;
TODO_AARCH64();
#elif ARCH(RISCV64)
(void)value;
TODO_RISCV64();
// NOTE: RISC-V LP64 specifies long long == long.
return static_cast<long long>(lrintf(value));
#elif ARCH(X86_64)
long long res;
asm(
@ -680,16 +717,34 @@ long double frexpl(long double x, int* exp) NOEXCEPT
return scalbnl(x, -(*exp));
}
#if !(ARCH(X86_64))
double round(double value) NOEXCEPT
double round(double x) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
#if ARCH(RISCV64)
if (fabs(x) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.d %0, %1, rmm"
: "=r"(output)
: "f"(x));
return static_cast<double>(output);
}
#endif
return internal_to_integer(x, RoundingMode::ToEven);
}
float roundf(float value) NOEXCEPT
float roundf(float x) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
#if ARCH(RISCV64)
if (fabsf(x) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.s %0, %1, rmm"
: "=r"(output)
: "f"(x));
return static_cast<float>(output);
}
#endif
return internal_to_integer(x, RoundingMode::ToEven);
}
long double roundl(long double value) NOEXCEPT
@ -699,175 +754,164 @@ long double roundl(long double value) NOEXCEPT
long lroundf(float value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
long lround(double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
long lroundl(long double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
long long llroundf(float value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
long long llround(double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
long long llroundd(long double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
float floorf(float value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::Down);
}
double floor(double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::Down);
}
long double floorl(long double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::Down);
}
float ceilf(float value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::Up);
}
double ceil(double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::Up);
}
long double ceill(long double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::Up);
}
#else
double round(double x) NOEXCEPT
{
// Note: This is break-tie-away-from-zero, so not the hw's understanding of
// "nearest", which would be towards even.
if (x == 0.)
return x;
if (x > 0.)
return floor(x + .5);
return ceil(x - .5);
}
float roundf(float x) NOEXCEPT
{
if (x == 0.f)
return x;
if (x > 0.f)
return floorf(x + .5f);
return ceilf(x - .5f);
}
long double roundl(long double x) NOEXCEPT
{
if (x == 0.L)
return x;
if (x > 0.L)
return floorl(x + .5L);
return ceill(x - .5L);
}
long lroundf(float value) NOEXCEPT
{
return static_cast<long>(roundf(value));
}
long lround(double value) NOEXCEPT
{
return static_cast<long>(round(value));
}
long lroundl(long double value) NOEXCEPT
{
return static_cast<long>(roundl(value));
}
long long llroundf(float value) NOEXCEPT
{
return static_cast<long long>(roundf(value));
}
long long llround(double value) NOEXCEPT
{
return static_cast<long long>(round(value));
}
long long llroundd(long double value) NOEXCEPT
{
return static_cast<long long>(roundl(value));
}
float floorf(float value) NOEXCEPT
{
AK::X87RoundingModeScope scope { AK::RoundingMode::DOWN };
asm("frndint"
: "+t"(value));
return value;
}
double floor(double value) NOEXCEPT
{
AK::X87RoundingModeScope scope { AK::RoundingMode::DOWN };
asm("frndint"
: "+t"(value));
return value;
}
long double floorl(long double value) NOEXCEPT
{
AK::X87RoundingModeScope scope { AK::RoundingMode::DOWN };
asm("frndint"
: "+t"(value));
return value;
}
float ceilf(float value) NOEXCEPT
{
AK::X87RoundingModeScope scope { AK::RoundingMode::UP };
asm("frndint"
: "+t"(value));
return value;
}
double ceil(double value) NOEXCEPT
{
AK::X87RoundingModeScope scope { AK::RoundingMode::UP };
asm("frndint"
: "+t"(value));
return value;
}
long double ceill(long double value) NOEXCEPT
{
AK::X87RoundingModeScope scope { AK::RoundingMode::UP };
asm("frndint"
: "+t"(value));
return value;
}
#if ARCH(RISCV64)
i64 output;
asm("fcvt.l.s %0, %1, rmm"
: "=r"(output)
: "f"(value));
return output;
#endif
return internal_to_integer(value, RoundingMode::ToEven);
}
long lround(double value) NOEXCEPT
{
#if ARCH(RISCV64)
i64 output;
asm("fcvt.l.d %0, %1, rmm"
: "=r"(output)
: "f"(value));
return output;
#endif
return internal_to_integer(value, RoundingMode::ToEven);
}
long lroundl(long double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
long long llroundf(float value) NOEXCEPT
{
#if ARCH(RISCV64)
i64 output;
asm("fcvt.l.s %0, %1, rmm"
: "=r"(output)
: "f"(value));
return output;
#endif
return internal_to_integer(value, RoundingMode::ToEven);
}
long long llround(double value) NOEXCEPT
{
#if ARCH(RISCV64)
i64 output;
asm("fcvt.l.d %0, %1, rmm"
: "=r"(output)
: "f"(value));
return output;
#endif
return internal_to_integer(value, RoundingMode::ToEven);
}
long long llroundd(long double value) NOEXCEPT
{
return internal_to_integer(value, RoundingMode::ToEven);
}
float floorf(float value) NOEXCEPT
{
#if ARCH(RISCV64)
if (fabsf(value) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.s %0, %1, rdn"
: "=r"(output)
: "f"(value));
return static_cast<float>(output);
}
#elif ARCH(X86_64)
AK::X87RoundingModeScope scope { AK::RoundingMode::DOWN };
asm("frndint"
: "+t"(value));
return value;
#endif
return internal_to_integer(value, RoundingMode::Down);
}
double floor(double value) NOEXCEPT
{
#if ARCH(RISCV64)
if (fabs(value) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.d %0, %1, rdn"
: "=r"(output)
: "f"(value));
return static_cast<double>(output);
}
#elif ARCH(X86_64)
AK::X87RoundingModeScope scope { AK::RoundingMode::DOWN };
asm("frndint"
: "+t"(value));
return value;
#endif
return internal_to_integer(value, RoundingMode::Down);
}
long double floorl(long double value) NOEXCEPT
{
#if ARCH(X86_64)
AK::X87RoundingModeScope scope { AK::RoundingMode::DOWN };
asm("frndint"
: "+t"(value));
return value;
#endif
return internal_to_integer(value, RoundingMode::Down);
}
float ceilf(float value) NOEXCEPT
{
#if ARCH(RISCV64)
if (fabsf(value) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.s %0, %1, rup"
: "=r"(output)
: "f"(value));
return static_cast<float>(output);
}
#elif ARCH(X86_64)
AK::X87RoundingModeScope scope { AK::RoundingMode::UP };
asm("frndint"
: "+t"(value));
return value;
#endif
return internal_to_integer(value, RoundingMode::Up);
}
double ceil(double value) NOEXCEPT
{
#if ARCH(RISCV64)
if (fabs(value) < LONG_LONG_MAX) {
i64 output;
asm("fcvt.l.d %0, %1, rup"
: "=r"(output)
: "f"(value));
return static_cast<double>(output);
}
#elif ARCH(X86_64)
AK::X87RoundingModeScope scope { AK::RoundingMode::UP };
asm("frndint"
: "+t"(value));
return value;
#endif
return internal_to_integer(value, RoundingMode::Up);
}
long double ceill(long double value) NOEXCEPT
{
#if ARCH(X86_64)
AK::X87RoundingModeScope scope { AK::RoundingMode::UP };
asm("frndint"
: "+t"(value));
return value;
#endif
return internal_to_integer(value, RoundingMode::Up);
}
long double modfl(long double x, long double* intpart) NOEXCEPT
{
return internal_modf(x, intpart);