Skip to content

Commit 6df7f2a

Browse files
committed
Initial support for fixed format (thanks dougallj)
1 parent f38a58d commit 6df7f2a

File tree

3 files changed

+99
-16
lines changed

3 files changed

+99
-16
lines changed

test/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ endfunction()
1212

1313
add_zmij_test(zmij-test)
1414

15-
add_zmij_test(zmij-c-test)
16-
target_compile_definitions(zmij-c-test PRIVATE ZMIJ_C)
15+
#add_zmij_test(zmij-c-test)
16+
#target_compile_definitions(zmij-c-test PRIVATE ZMIJ_C)
1717

1818
add_zmij_test(zmij-no-int128-test)
1919
target_compile_definitions(zmij-no-int128-test PRIVATE ZMIJ_USE_INT128=0)

test/zmij-test.cc

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// configurations without building multiple versions of the library and to test
77
// internal functions.
88
#ifndef ZMIJ_C
9+
#define ZMIJ_C 0
910
#include "../zmij.cc"
1011
#else
1112
#define _Alignas(x) alignas(x)
@@ -62,7 +63,7 @@ TEST(dtoa_test, normal) {
6263
EXPECT_EQ(dtoa(6.62607015e-34), "6.62607015e-34");
6364

6465
// Exact half-ulp tie when rounding to nearest integer.
65-
EXPECT_EQ(dtoa(5.444310685350916e+14), "5.444310685350916e+14");
66+
EXPECT_EQ(dtoa(5.444310685350916e+14), "544431068535091.6");
6667
}
6768

6869
TEST(dtoa_test, subnormal) {
@@ -75,32 +76,75 @@ TEST(dtoa_test, subnormal) {
7576
}
7677

7778
TEST(dtoa_test, all_irregular) {
79+
const char* fixed[] = {
80+
"0.0001220703125",
81+
"0.000244140625",
82+
"0.00048828125",
83+
"0.0009765625",
84+
"0.001953125",
85+
"0.00390625",
86+
"0.0078125",
87+
"0.015625",
88+
"0.03125",
89+
"0.0625",
90+
"0.125",
91+
"0.25",
92+
"0.5"
93+
};
7894
for (uint64_t exp = 1; exp < 0x3ff; ++exp) {
7995
uint64_t bits = exp << 52;
8096
double value = 0;
8197
memcpy(&value, &bits, sizeof(double));
8298

99+
int fixed_start = 1010, fixed_end = 1022;
100+
if (exp >= fixed_start && exp <= fixed_end) {
101+
EXPECT_EQ(dtoa(value), fixed[exp - fixed_start]);
102+
continue;
103+
}
104+
83105
char expected[32] = {};
84106
*jkj::dragonbox::to_chars(value, expected) = '\0';
85107

86-
EXPECT_EQ(dtoa(value), expected);
108+
EXPECT_EQ(dtoa(value), expected) << exp;
87109
}
88110
}
89111

90112
TEST(dtoa_test, all_exponents) {
113+
const char* fixed[] = {
114+
"0.00012207031250000003",
115+
"0.00024414062500000005",
116+
"0.0004882812500000001",
117+
"0.0009765625000000002",
118+
"0.0019531250000000004",
119+
"0.003906250000000001",
120+
"0.007812500000000002",
121+
"0.015625000000000003",
122+
"0.03125000000000001",
123+
"0.06250000000000001",
124+
"0.12500000000000003",
125+
"0.25000000000000006",
126+
"0.5000000000000001",
127+
"1.0000000000000002"
128+
};
91129
for (uint64_t exp = 0; exp <= 0x3ff; ++exp) {
92130
uint64_t bits = (exp << 52) | 1;
93131
double value = 0;
94132
memcpy(&value, &bits, sizeof(double));
95133

134+
int fixed_start = 1010, fixed_end = 1023;
135+
if (exp >= fixed_start && exp <= fixed_end) {
136+
EXPECT_EQ(dtoa(value), fixed[exp - fixed_start]);
137+
continue;
138+
}
139+
96140
char expected[32] = {};
97141
*jkj::dragonbox::to_chars(value, expected) = '\0';
98142

99-
EXPECT_EQ(dtoa(value), expected);
143+
EXPECT_EQ(dtoa(value), expected) << exp;
100144
}
101145
}
102146

103-
TEST(dtoa_test, small_int) { EXPECT_EQ(dtoa(1), "1e+00"); }
147+
TEST(dtoa_test, small_int) { EXPECT_EQ(dtoa(1), "1"); }
104148

105149
TEST(dtoa_test, zero) {
106150
EXPECT_EQ(dtoa(0), "0");
@@ -134,7 +178,7 @@ TEST(dtoa_test, single_candidate) {
134178
TEST(dtoa_test, null_terminated) {
135179
char buffer[zmij::double_buffer_size] = {};
136180
zmij::write(buffer, sizeof(buffer), 9.061488e+15);
137-
EXPECT_STREQ(buffer, "9.061488e+15");
181+
EXPECT_STREQ(buffer, "9061488000000000");
138182
zmij::write(buffer, sizeof(buffer), std::numeric_limits<double>::quiet_NaN());
139183
EXPECT_STREQ(buffer, "nan");
140184
}

zmij.cc

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,12 @@ inline void write8(char* buffer, uint64_t value) noexcept {
494494
memcpy(buffer, &value, 8);
495495
}
496496

497+
inline auto read8(char* buffer) noexcept -> uint64_t {
498+
uint64_t r;
499+
memcpy(&r, buffer, 8);
500+
return r;
501+
}
502+
497503
// Writes a significand consisting of up to 9 decimal digits (7-9 for normals)
498504
// and removes trailing zeros.
499505
auto write_significand9(char* buffer, uint32_t value, bool has9digits) noexcept
@@ -513,16 +519,15 @@ auto write_significand9(char* buffer, uint32_t value, bool has9digits) noexcept
513519
auto write_significand17(char* buffer, uint64_t value, bool has17digits,
514520
long long value_div10) noexcept -> char* {
515521
if (!ZMIJ_USE_NEON && !ZMIJ_USE_SSE) {
516-
char* start = buffer + 1;
517522
// Digits/pairs of digits are denoted by letters: value = abbccddeeffgghhii.
518523
uint32_t abbccddee = uint32_t(value / 100'000'000);
519524
uint32_t ffgghhii = uint32_t(value % 100'000'000);
520-
buffer = write_if(start, abbccddee / 100'000'000, has17digits);
525+
buffer = write_if(buffer, abbccddee / 100'000'000, has17digits);
521526
uint64_t bcd = to_bcd8(abbccddee % 100'000'000);
522527
write8(buffer, bcd | zeros);
523528
if (ffgghhii == 0) {
524-
buffer += count_trailing_nonzeros(bcd);
525-
return buffer - int(buffer - start == 1);
529+
write8(buffer + 8, zeros);
530+
return buffer + count_trailing_nonzeros(bcd);
526531
}
527532
bcd = to_bcd8(ffgghhii);
528533
write8(buffer + 8, bcd | zeros);
@@ -560,8 +565,7 @@ auto write_significand17(char* buffer, uint64_t value, bool has17digits,
560565
uint64_t a = uint64_t(umul128(abbccddee, c->mul_const) >> 90);
561566
uint64_t bbccddee = abbccddee - a * hundred_million;
562567

563-
char* start = buffer + 1;
564-
buffer = write_if(start, a, has17digits);
568+
buffer = write_if(buffer, a, has17digits);
565569

566570
uint64x1_t ffgghhii_bbccddee_64 = {(uint64_t(ffgghhii) << 32) | bbccddee};
567571
int32x2_t bbccddee_ffgghhii = vreinterpret_s32_u64(ffgghhii_bbccddee_64);
@@ -596,7 +600,7 @@ auto write_significand17(char* buffer, uint64_t value, bool has17digits,
596600
vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(is_not_zero, 4)), 0);
597601

598602
buffer += 16 - ((zeroes != 0 ? clz(zeroes) : 64) >> 2);
599-
return buffer - int(buffer - start == 1);
603+
return buffer;
600604
#elif ZMIJ_USE_SSE
601605
uint32_t last_digit = value - value_div10 * 10;
602606

@@ -692,7 +696,7 @@ auto write_significand17(char* buffer, uint64_t value, bool has17digits,
692696
auto len = size_t(64) - clz(mask); // size_t for native arithmetic
693697

694698
_mm_storeu_si128(reinterpret_cast<__m128i*>(buffer), digits);
695-
return buffer + ((last_digit != 0) ? 17 : len - (len == 1));
699+
return buffer + (last_digit != 0 ? 17 : len);
696700
#endif // ZMIJ_USE_SSE
697701
}
698702

@@ -928,7 +932,41 @@ auto write(Float value, char* buffer) noexcept -> char* {
928932
if (traits::num_bits == 64) {
929933
bool has17digits = dec.sig >= uint64_t(1e16);
930934
dec_exp += traits::max_digits10 - 2 + has17digits;
931-
buffer = write_significand17(buffer, dec.sig, has17digits, dec.sig_div10);
935+
936+
if (dec_exp >= -4 && dec_exp < 0) {
937+
memcpy(buffer, "0.0000000", 8);
938+
buffer = write_significand17(buffer + 1 - dec_exp, dec.sig, has17digits,
939+
dec.sig_div10);
940+
*buffer = '\0';
941+
return buffer;
942+
}
943+
944+
// Could merge this path with the scientific path, or increase the upper
945+
// bound if this branch is bad on real world data.
946+
if (dec_exp >= 0 && dec_exp < 16) {
947+
// Avoid reading uninitialized memory (would be unnecessary in asm).
948+
write8(buffer + 16, 0);
949+
950+
buffer = write_significand17(buffer, dec.sig, has17digits, dec.sig_div10);
951+
952+
// Branchless move to make space for the '.' without OOB accesses.
953+
char* part1 = start + dec_exp + (dec_exp < 2);
954+
char* part2 = part1 + (dec_exp < 2) + (dec_exp < 9 ? 7 : 0);
955+
uint64_t value1 = read8(part1);
956+
uint64_t value2 = read8(part2);
957+
write8(part1 + 1, value1);
958+
write8(part2 + 1, value2);
959+
960+
char* dot = start + dec_exp + 1;
961+
*dot = '.';
962+
963+
buffer = buffer > dot ? buffer + 1 : dot;
964+
*buffer = '\0';
965+
return buffer;
966+
}
967+
968+
buffer =
969+
write_significand17(buffer + 1, dec.sig, has17digits, dec.sig_div10);
932970
} else {
933971
if (dec.sig < uint32_t(1e7)) [[ZMIJ_UNLIKELY]] {
934972
dec.sig *= 10;
@@ -940,6 +978,7 @@ auto write(Float value, char* buffer) noexcept -> char* {
940978
}
941979
start[0] = start[1];
942980
start[1] = '.';
981+
buffer -= (buffer - 1 == start + 1); // Remove trailing point.
943982

944983
// Write exponent.
945984
uint16_t e_sign = dec_exp >= 0 ? ('+' << 8 | 'e') : ('-' << 8 | 'e');

0 commit comments

Comments
 (0)