Skip to content

Commit cf6fb4d

Browse files
Nicolas Pitrenashif
authored andcommitted
lib: cbprintf: float conversion optimization and documentation
While documenting the float conversion code, I found there was room for some optimization. In doing so I added test cases to cover edge cases e.g. making sure proper rounding is applied and that no loss of precision was introduced. Compiled code should be smaller and faster. Signed-off-by: Nicolas Pitre <[email protected]>
1 parent adb8087 commit cf6fb4d

File tree

2 files changed

+143
-60
lines changed

2 files changed

+143
-60
lines changed

lib/os/cbprintf_complete.c

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -695,22 +695,10 @@ static size_t conversion_arglen(const struct conversion *conv)
695695
return words;
696696
}
697697

698-
/* Ceiling divide by two. */
699-
static void _rlrshift(uint64_t *v)
700-
{
701-
*v = (*v & 1) + (*v >> 1);
702-
}
703-
704698
#ifdef CONFIG_64BIT
705699

706700
static void _ldiv5(uint64_t *v)
707701
{
708-
/*
709-
* Usage in this file wants rounded behavior, not truncation. So add
710-
* two to get the threshold right.
711-
*/
712-
*v += 2U;
713-
714702
/* The compiler can optimize this on its own on 64-bit architectures */
715703
*v /= 5U;
716704
}
@@ -735,13 +723,11 @@ static void _ldiv5(uint64_t *v)
735723
*
736724
* Here the multiplier is: (1 << 64) / 5 = 0x3333333333333333
737725
* i.e. a 62 bits value. To compensate for the reduced precision, we
738-
* add an initial bias of 1 to v. Enlarging the multiplier to 64 bits
739-
* would also work but a final right shift would be needed, and carry
740-
* handling on the summing of partial mults would be necessary, requiring
741-
* more instructions. Given that we already want to add bias of 2 for
742-
* the result to be rounded to nearest and not truncated, we might as well
743-
* combine those together into a bias of 3. This also conveniently allows
744-
* for keeping the multiplier in a single 32-bit register given its pattern.
726+
* add an initial bias of 1 to v. This conveniently allows for keeping
727+
* the multiplier in a single 32-bit register given its pattern.
728+
* Enlarging the multiplier to 64 bits would also work but carry handling
729+
* on the summing of partial mults would be necessary, and a final right
730+
* shift would be needed, requiring more instructions.
745731
*/
746732
static void _ldiv5(uint64_t *v)
747733
{
@@ -758,12 +744,10 @@ static void _ldiv5(uint64_t *v)
758744
__asm__ ("" : "+r" (m));
759745

760746
/*
761-
* Apply the bias of 3. We can't add it to v as this would overflow
747+
* Apply a bias of 1 to v. We can't add it to v as this would overflow
762748
* it when at max range. Factor it out with the multiplier upfront.
763-
* Here we multiply the low and high parts separately to avoid an
764-
* unnecessary 64-bit add-with-carry.
765749
*/
766-
result = ((uint64_t)(m * 3U) << 32) | (m * 3U);
750+
result = ((uint64_t)m << 32) | m;
767751

768752
/* The actual multiplication. */
769753
result += (uint64_t)v_lo * m;
@@ -778,6 +762,13 @@ static void _ldiv5(uint64_t *v)
778762

779763
#endif /* CONFIG_64BIT */
780764

765+
/* Division by 10 */
766+
static void _ldiv10(uint64_t *v)
767+
{
768+
*v >>= 1;
769+
_ldiv5(v);
770+
}
771+
781772
/* Extract the next decimal character in the converted representation of a
782773
* fractional component.
783774
*/
@@ -855,9 +846,6 @@ static char *encode_uint(uint_value_type value,
855846
return bp;
856847
}
857848

858-
/* A magic value used in conversion. */
859-
#define MAX_FP1 UINT32_MAX
860-
861849
/* Number of bits in the fractional part of an IEEE 754-2008 double
862850
* precision float.
863851
*/
@@ -1110,41 +1098,56 @@ static char *encode_float(double value,
11101098
fract |= BIT_63;
11111099
}
11121100

1113-
1114-
/* Magically convert the base-2 exponent to a base-10
1115-
* exponent.
1101+
/*
1102+
* Let's consider:
1103+
*
1104+
* value = fract * 2^exp * 10^decexp
1105+
*
1106+
* Initially decexp = 0. The goal is to bring exp between
1107+
* 0 and -2 as the magnitude of a fractional decimal digit is 3 bits.
11161108
*/
11171109
int decexp = 0;
11181110

1119-
while (exp <= -3) {
1120-
while ((fract >> 32) >= (MAX_FP1 / 5)) {
1121-
_rlrshift(&fract);
1111+
while (exp < -2) {
1112+
/*
1113+
* Make roon to allow a multiplication by 5 without overflow.
1114+
* We test only the top part for faster code.
1115+
*/
1116+
do {
1117+
fract >>= 1;
11221118
exp++;
1123-
}
1119+
} while ((uint32_t)(fract >> 32) >= (UINT32_MAX / 5U));
1120+
1121+
/* Perform fract * 5 * 2 / 10 */
11241122
fract *= 5U;
11251123
exp++;
11261124
decexp--;
1127-
1128-
while ((fract >> 32) <= (MAX_FP1 / 2)) {
1129-
fract <<= 1;
1130-
exp--;
1131-
}
11321125
}
11331126

11341127
while (exp > 0) {
1128+
/*
1129+
* Perform fract / 5 / 2 * 10.
1130+
* The +2 is there to do round the result of the division
1131+
* by 5 not to lose too much precision in extreme cases.
1132+
*/
1133+
fract += 2;
11351134
_ldiv5(&fract);
11361135
exp--;
11371136
decexp++;
1138-
while ((fract >> 32) <= (MAX_FP1 / 2)) {
1137+
1138+
/* Bring back our fractional number to full scale */
1139+
do {
11391140
fract <<= 1;
11401141
exp--;
1141-
}
1142+
} while (!(fract & BIT_63));
11421143
}
11431144

1144-
while (exp < (0 + 4)) {
1145-
_rlrshift(&fract);
1146-
exp++;
1147-
}
1145+
/*
1146+
* The binary fractional point is located somewhere above bit 63.
1147+
* Move it between bits 59 and 60 to give 4 bits of room to the
1148+
* integer part.
1149+
*/
1150+
fract >>= (4 - exp);
11481151

11491152
if ((c == 'g') || (c == 'G')) {
11501153
/* Use the specified precision and exponent to select the
@@ -1165,32 +1168,31 @@ static char *encode_float(double value,
11651168
}
11661169
}
11671170

1171+
int decimals;
11681172
if (c == 'f') {
1169-
exp = precision + decexp;
1170-
if (exp < 0) {
1171-
exp = 0;
1173+
decimals = precision + decexp;
1174+
if (decimals < 0) {
1175+
decimals = 0;
11721176
}
11731177
} else {
1174-
exp = precision + 1;
1178+
decimals = precision + 1;
11751179
}
11761180

11771181
int digit_count = 16;
11781182

1179-
if (exp > 16) {
1180-
exp = 16;
1183+
if (decimals > 16) {
1184+
decimals = 16;
11811185
}
11821186

1183-
uint64_t ltemp = BIT64(59);
1184-
1185-
while (exp--) {
1186-
_ldiv5(&ltemp);
1187-
_rlrshift(&ltemp);
1187+
/* Round the value to the last digit being printed. */
1188+
uint64_t round = BIT64(59); /* 0.5 */
1189+
while (decimals--) {
1190+
_ldiv10(&round);
11881191
}
1189-
1190-
fract += ltemp;
1191-
if ((fract >> 32) & (0x0FU << 28)) {
1192-
_ldiv5(&fract);
1193-
_rlrshift(&fract);
1192+
fract += round;
1193+
/* Make sure rounding didn't make fract >= 1.0 */
1194+
if (fract >= BIT64(60)) {
1195+
_ldiv10(&fract);
11941196
decexp++;
11951197
}
11961198

tests/unit/cbprintf/main.c

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,87 @@ static void test_fp_value(void)
718718
} else {
719719
PRF_CHECK("%a 5.562685e-309", rc);
720720
}
721+
722+
/*
723+
* The following tests are tailored to exercise edge cases in
724+
* lib/os/cbprintf_complete.c:encode_float() and related functions.
725+
*/
726+
727+
dv = 0x1.0p-3;
728+
rc = TEST_PRF("%.16g", dv);
729+
PRF_CHECK("0.125", rc);
730+
731+
dv = 0x1.0p-4;
732+
rc = TEST_PRF("%.16g", dv);
733+
PRF_CHECK("0.0625", rc);
734+
735+
dv = 0x1.8p-4;
736+
rc = TEST_PRF("%.16g", dv);
737+
PRF_CHECK("0.09375", rc);
738+
739+
dv = 0x1.cp-4;
740+
rc = TEST_PRF("%.16g", dv);
741+
PRF_CHECK("0.109375", rc);
742+
743+
dv = 0x1.9999999ffffffp-8;
744+
rc = TEST_PRF("%.16g", dv);
745+
PRF_CHECK("0.006250000005820765", rc);
746+
747+
dv = 0x1.0p+0;
748+
rc = TEST_PRF("%.16g", dv);
749+
PRF_CHECK("1", rc);
750+
751+
dv = 0x1.fffffffffffffp-1022;
752+
rc = TEST_PRF("%.16g", dv);
753+
PRF_CHECK("4.450147717014402e-308", rc);
754+
755+
dv = 0x1.ffffffffffffep-1022;
756+
rc = TEST_PRF("%.16g", dv);
757+
PRF_CHECK("4.450147717014402e-308", rc);
758+
759+
dv = 0x1.ffffffffffffdp-1022;
760+
rc = TEST_PRF("%.16g", dv);
761+
PRF_CHECK("4.450147717014401e-308", rc);
762+
763+
dv = 0x1.0000000000001p-1022;
764+
rc = TEST_PRF("%.16g", dv);
765+
PRF_CHECK("2.225073858507202e-308", rc);
766+
767+
dv = 0x1p-1022;
768+
rc = TEST_PRF("%.16g", dv);
769+
PRF_CHECK("2.225073858507201e-308", rc);
770+
771+
dv = 0x0.fffffffffffffp-1022;
772+
rc = TEST_PRF("%.16g", dv);
773+
PRF_CHECK("2.225073858507201e-308", rc);
774+
775+
dv = 0x0.0000000000001p-1022;
776+
rc = TEST_PRF("%.16g", dv);
777+
PRF_CHECK("4.940656458412465e-324", rc);
778+
779+
dv = 0x1.1fa182c40c60dp-1019;
780+
rc = TEST_PRF("%.16g", dv);
781+
PRF_CHECK("2e-307", rc);
782+
783+
dv = 0x1.fffffffffffffp+1023;
784+
rc = TEST_PRF("%.16g", dv);
785+
PRF_CHECK("1.797693134862316e+308", rc);
786+
787+
dv = 0x1.ffffffffffffep+1023;
788+
rc = TEST_PRF("%.16g", dv);
789+
PRF_CHECK("1.797693134862316e+308", rc);
790+
791+
dv = 0x1.ffffffffffffdp+1023;
792+
rc = TEST_PRF("%.16g", dv);
793+
PRF_CHECK("1.797693134862315e+308", rc);
794+
795+
dv = 0x1.0000000000001p+1023;
796+
rc = TEST_PRF("%.16g", dv);
797+
PRF_CHECK("8.988465674311582e+307", rc);
798+
799+
dv = 0x1p+1023;
800+
rc = TEST_PRF("%.16g", dv);
801+
PRF_CHECK("8.98846567431158e+307", rc);
721802
}
722803

723804
static void test_fp_length(void)

0 commit comments

Comments
 (0)