Skip to content

Commit 2614b12

Browse files
authored
Merge pull request #767 from radiospiel/eno/pr/fixnums
Faster integer formatting
2 parents 00e3e28 + 1bcebef commit 2614b12

File tree

4 files changed

+335
-14
lines changed

4 files changed

+335
-14
lines changed

ext/json/ext/fbuffer/fbuffer.h

+45-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "ruby.h"
55
#include "ruby/encoding.h"
6+
#include "../vendor/jeaiii-ltoa.h"
67

78
/* shims */
89
/* This is the fallback definition from Ruby 3.4 */
@@ -150,6 +151,13 @@ static void fbuffer_append(FBuffer *fb, const char *newstr, unsigned long len)
150151
}
151152
}
152153

154+
/* Appends a character into a buffer. The buffer needs to have sufficient capacity, via fbuffer_inc_capa(...). */
155+
static inline void fbuffer_append_reserved_char(FBuffer *fb, char chr)
156+
{
157+
fb->ptr[fb->len] = chr;
158+
fb->len += 1;
159+
}
160+
153161
static void fbuffer_append_str(FBuffer *fb, VALUE str)
154162
{
155163
const char *newstr = StringValuePtr(str);
@@ -167,25 +175,48 @@ static inline void fbuffer_append_char(FBuffer *fb, char newchr)
167175
fb->len++;
168176
}
169177

170-
static long fltoa(long number, char *buf)
178+
static inline char *fbuffer_cursor(FBuffer *fb)
179+
{
180+
return fb->ptr + fb->len;
181+
}
182+
183+
static inline void fbuffer_advance_to(FBuffer *fb, char *end)
171184
{
172-
static const char digits[] = "0123456789";
173-
long sign = number;
174-
char* tmp = buf;
175-
176-
if (sign < 0) number = -number;
177-
do *tmp-- = digits[number % 10]; while (number /= 10);
178-
if (sign < 0) *tmp-- = '-';
179-
return buf - tmp;
185+
fb->len = end - fb->ptr;
180186
}
181187

182-
#define LONG_BUFFER_SIZE 20
188+
/*
189+
* Appends the decimal string representation of \a number into the buffer.
190+
*/
183191
static void fbuffer_append_long(FBuffer *fb, long number)
184192
{
185-
char buf[LONG_BUFFER_SIZE];
186-
char *buffer_end = buf + LONG_BUFFER_SIZE;
187-
long len = fltoa(number, buffer_end - 1);
188-
fbuffer_append(fb, buffer_end - len, len);
193+
/*
194+
* The jeaiii_ultoa() function produces digits left-to-right,
195+
* allowing us to write directly into the buffer, but we don't know
196+
* the number of resulting characters.
197+
*
198+
* We do know, however, that the `number` argument is always in the
199+
* range 0xc000000000000000 to 0x3fffffffffffffff, or, in decimal,
200+
* -4611686018427387904 to 4611686018427387903. The max number of chars
201+
* generated is therefore 20 (including a potential sign character).
202+
*/
203+
204+
static const int MAX_CHARS_FOR_LONG = 20;
205+
206+
fbuffer_inc_capa(fb, MAX_CHARS_FOR_LONG);
207+
208+
if (number < 0) {
209+
fbuffer_append_reserved_char(fb, '-');
210+
211+
/*
212+
* Since number is always > LONG_MIN, `-number` will not overflow
213+
* and is always the positive abs() value.
214+
*/
215+
number = -number;
216+
}
217+
218+
char *end = jeaiii_ultoa(fbuffer_cursor(fb), number);
219+
fbuffer_advance_to(fb, end);
189220
}
190221

191222
static VALUE fbuffer_finalize(FBuffer *fb)

ext/json/ext/generator/depend

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
generator.o: generator.c $(srcdir)/../fbuffer/fbuffer.h
22
generator.o: generator.c $(srcdir)/../vendor/fpconv.c
3+
generator.o: generator.c $(srcdir)/../vendor/jeaiii-ltoa.h

ext/json/ext/vendor/jeaiii-ltoa.h

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/*
2+
3+
This file is released under the terms of the MIT License. It is based on the
4+
work of James Edward Anhalt III, with the original license listed below.
5+
6+
MIT License
7+
8+
Copyright (c) 2024,2025 Enrico Thierbach - https://github.com/radiospiel
9+
Copyright (c) 2022 James Edward Anhalt III - https://github.com/jeaiii/itoa
10+
11+
Permission is hereby granted, free of charge, to any person obtaining a copy
12+
of this software and associated documentation files (the "Software"), to deal
13+
in the Software without restriction, including without limitation the rights
14+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
copies of the Software, and to permit persons to whom the Software is
16+
furnished to do so, subject to the following conditions:
17+
18+
The above copyright notice and this permission notice shall be included in all
19+
copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27+
SOFTWARE.
28+
*/
29+
30+
#ifndef JEAIII_TO_TEXT_H_
31+
#define JEAIII_TO_TEXT_H_
32+
33+
#include <stdint.h>
34+
35+
typedef uint_fast32_t u32_t;
36+
typedef uint_fast64_t u64_t;
37+
38+
#pragma clang diagnostic push
39+
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
40+
41+
#pragma GCC diagnostic push
42+
#pragma GCC diagnostic ignored "-Wmissing-braces"
43+
44+
#define u32(x) ((u32_t)(x))
45+
#define u64(x) ((u64_t)(x))
46+
47+
struct digit_pair
48+
{
49+
char dd[2];
50+
};
51+
52+
static const struct digit_pair *digits_dd = (struct digit_pair *)(
53+
"00" "01" "02" "03" "04" "05" "06" "07" "08" "09"
54+
"10" "11" "12" "13" "14" "15" "16" "17" "18" "19"
55+
"20" "21" "22" "23" "24" "25" "26" "27" "28" "29"
56+
"30" "31" "32" "33" "34" "35" "36" "37" "38" "39"
57+
"40" "41" "42" "43" "44" "45" "46" "47" "48" "49"
58+
"50" "51" "52" "53" "54" "55" "56" "57" "58" "59"
59+
"60" "61" "62" "63" "64" "65" "66" "67" "68" "69"
60+
"70" "71" "72" "73" "74" "75" "76" "77" "78" "79"
61+
"80" "81" "82" "83" "84" "85" "86" "87" "88" "89"
62+
"90" "91" "92" "93" "94" "95" "96" "97" "98" "99"
63+
);
64+
65+
static const struct digit_pair *digits_fd = (struct digit_pair *)(
66+
"0_" "1_" "2_" "3_" "4_" "5_" "6_" "7_" "8_" "9_"
67+
"10" "11" "12" "13" "14" "15" "16" "17" "18" "19"
68+
"20" "21" "22" "23" "24" "25" "26" "27" "28" "29"
69+
"30" "31" "32" "33" "34" "35" "36" "37" "38" "39"
70+
"40" "41" "42" "43" "44" "45" "46" "47" "48" "49"
71+
"50" "51" "52" "53" "54" "55" "56" "57" "58" "59"
72+
"60" "61" "62" "63" "64" "65" "66" "67" "68" "69"
73+
"70" "71" "72" "73" "74" "75" "76" "77" "78" "79"
74+
"80" "81" "82" "83" "84" "85" "86" "87" "88" "89"
75+
"90" "91" "92" "93" "94" "95" "96" "97" "98" "99"
76+
);
77+
78+
static const u64_t mask24 = (u64(1) << 24) - 1;
79+
static const u64_t mask32 = (u64(1) << 32) - 1;
80+
static const u64_t mask57 = (u64(1) << 57) - 1;
81+
82+
#define COPY(buffer, digits) memcpy(buffer, &(digits), sizeof(struct digit_pair))
83+
84+
static char *
85+
jeaiii_ultoa(char *b, u64_t n)
86+
{
87+
if (n < u32(1e2)) {
88+
COPY(b, digits_fd[n]);
89+
return n < 10 ? b + 1 : b + 2;
90+
}
91+
92+
if (n < u32(1e6)) {
93+
if (n < u32(1e4)) {
94+
u32_t f0 = u32(10 * (1 << 24) / 1e3 + 1) * n;
95+
COPY(b, digits_fd[f0 >> 24]);
96+
97+
b -= n < u32(1e3);
98+
u32_t f2 = (f0 & mask24) * 100;
99+
COPY(b + 2, digits_dd[f2 >> 24]);
100+
101+
return b + 4;
102+
}
103+
104+
u64_t f0 = u64(10 * (1ull << 32ull)/ 1e5 + 1) * n;
105+
COPY(b, digits_fd[f0 >> 32]);
106+
107+
b -= n < u32(1e5);
108+
u64_t f2 = (f0 & mask32) * 100;
109+
COPY(b + 2, digits_dd[f2 >> 32]);
110+
111+
u64_t f4 = (f2 & mask32) * 100;
112+
COPY(b + 4, digits_dd[f4 >> 32]);
113+
return b + 6;
114+
}
115+
116+
if (n < u64(1ull << 32ull)) {
117+
if (n < u32(1e8)) {
118+
u64_t f0 = u64(10 * (1ull << 48ull) / 1e7 + 1) * n >> 16;
119+
COPY(b, digits_fd[f0 >> 32]);
120+
121+
b -= n < u32(1e7);
122+
u64_t f2 = (f0 & mask32) * 100;
123+
COPY(b + 2, digits_dd[f2 >> 32]);
124+
125+
u64_t f4 = (f2 & mask32) * 100;
126+
COPY(b + 4, digits_dd[f4 >> 32]);
127+
128+
u64_t f6 = (f4 & mask32) * 100;
129+
COPY(b + 6, digits_dd[f6 >> 32]);
130+
131+
return b + 8;
132+
}
133+
134+
u64_t f0 = u64(10 * (1ull << 57ull) / 1e9 + 1) * n;
135+
COPY(b, digits_fd[f0 >> 57]);
136+
137+
b -= n < u32(1e9);
138+
u64_t f2 = (f0 & mask57) * 100;
139+
COPY(b + 2, digits_dd[f2 >> 57]);
140+
141+
u64_t f4 = (f2 & mask57) * 100;
142+
COPY(b + 4, digits_dd[f4 >> 57]);
143+
144+
u64_t f6 = (f4 & mask57) * 100;
145+
COPY(b + 6, digits_dd[f6 >> 57]);
146+
147+
u64_t f8 = (f6 & mask57) * 100;
148+
COPY(b + 8, digits_dd[f8 >> 57]);
149+
150+
return b + 10;
151+
}
152+
153+
// if we get here U must be u64 but some compilers don't know that, so reassign n to a u64 to avoid warnings
154+
u32_t z = n % u32(1e8);
155+
u64_t u = n / u32(1e8);
156+
157+
if (u < u32(1e2)) {
158+
// u can't be 1 digit (if u < 10 it would have been handled above as a 9 digit 32bit number)
159+
COPY(b, digits_dd[u]);
160+
b += 2;
161+
}
162+
else if (u < u32(1e6)) {
163+
if (u < u32(1e4)) {
164+
u32_t f0 = u32(10 * (1 << 24) / 1e3 + 1) * u;
165+
COPY(b, digits_fd[f0 >> 24]);
166+
167+
b -= u < u32(1e3);
168+
u32_t f2 = (f0 & mask24) * 100;
169+
COPY(b + 2, digits_dd[f2 >> 24]);
170+
b += 4;
171+
}
172+
else {
173+
u64_t f0 = u64(10 * (1ull << 32ull) / 1e5 + 1) * u;
174+
COPY(b, digits_fd[f0 >> 32]);
175+
176+
b -= u < u32(1e5);
177+
u64_t f2 = (f0 & mask32) * 100;
178+
COPY(b + 2, digits_dd[f2 >> 32]);
179+
180+
u64_t f4 = (f2 & mask32) * 100;
181+
COPY(b + 4, digits_dd[f4 >> 32]);
182+
b += 6;
183+
}
184+
}
185+
else if (u < u32(1e8)) {
186+
u64_t f0 = u64(10 * (1ull << 48ull) / 1e7 + 1) * u >> 16;
187+
COPY(b, digits_fd[f0 >> 32]);
188+
189+
b -= u < u32(1e7);
190+
u64_t f2 = (f0 & mask32) * 100;
191+
COPY(b + 2, digits_dd[f2 >> 32]);
192+
193+
u64_t f4 = (f2 & mask32) * 100;
194+
COPY(b + 4, digits_dd[f4 >> 32]);
195+
196+
u64_t f6 = (f4 & mask32) * 100;
197+
COPY(b + 6, digits_dd[f6 >> 32]);
198+
199+
b += 8;
200+
}
201+
else if (u < u64(1ull << 32ull)) {
202+
u64_t f0 = u64(10 * (1ull << 57ull) / 1e9 + 1) * u;
203+
COPY(b, digits_fd[f0 >> 57]);
204+
205+
b -= u < u32(1e9);
206+
u64_t f2 = (f0 & mask57) * 100;
207+
COPY(b + 2, digits_dd[f2 >> 57]);
208+
209+
u64_t f4 = (f2 & mask57) * 100;
210+
COPY(b + 4, digits_dd[f4 >> 57]);
211+
212+
u64_t f6 = (f4 & mask57) * 100;
213+
COPY(b + 6, digits_dd[f6 >> 57]);
214+
215+
u64_t f8 = (f6 & mask57) * 100;
216+
COPY(b + 8, digits_dd[f8 >> 57]);
217+
b += 10;
218+
}
219+
else {
220+
u32_t y = u % u32(1e8);
221+
u /= u32(1e8);
222+
223+
// u is 2, 3, or 4 digits (if u < 10 it would have been handled above)
224+
if (u < u32(1e2)) {
225+
COPY(b, digits_dd[u]);
226+
b += 2;
227+
}
228+
else {
229+
u32_t f0 = u32(10 * (1 << 24) / 1e3 + 1) * u;
230+
COPY(b, digits_fd[f0 >> 24]);
231+
232+
b -= u < u32(1e3);
233+
u32_t f2 = (f0 & mask24) * 100;
234+
COPY(b + 2, digits_dd[f2 >> 24]);
235+
236+
b += 4;
237+
}
238+
// do 8 digits
239+
u64_t f0 = (u64((1ull << 48ull) / 1e6 + 1) * y >> 16) + 1;
240+
COPY(b, digits_dd[f0 >> 32]);
241+
242+
u64_t f2 = (f0 & mask32) * 100;
243+
COPY(b + 2, digits_dd[f2 >> 32]);
244+
245+
u64_t f4 = (f2 & mask32) * 100;
246+
COPY(b + 4, digits_dd[f4 >> 32]);
247+
248+
u64_t f6 = (f4 & mask32) * 100;
249+
COPY(b + 6, digits_dd[f6 >> 32]);
250+
b += 8;
251+
}
252+
253+
// do 8 digits
254+
u64_t f0 = (u64((1ull << 48ull) / 1e6 + 1) * z >> 16) + 1;
255+
COPY(b, digits_dd[f0 >> 32]);
256+
257+
u64_t f2 = (f0 & mask32) * 100;
258+
COPY(b + 2, digits_dd[f2 >> 32]);
259+
260+
u64_t f4 = (f2 & mask32) * 100;
261+
COPY(b + 4, digits_dd[f4 >> 32]);
262+
263+
u64_t f6 = (f4 & mask32) * 100;
264+
COPY(b + 6, digits_dd[f6 >> 32]);
265+
266+
return b + 8;
267+
}
268+
269+
#undef u32
270+
#undef u64
271+
#undef COPY
272+
273+
#pragma clang diagnostic pop
274+
#pragma GCC diagnostic pop
275+
276+
#endif // JEAIII_TO_TEXT_H_
277+

test/json/json_generator_test.rb

+12
Original file line numberDiff line numberDiff line change
@@ -707,4 +707,16 @@ def test_json_generate_float
707707
assert_equal expected, value.to_json
708708
end
709709
end
710+
711+
def test_numbers_of_various_sizes
712+
numbers = [
713+
0, 1, -1, 9, -9, 13, -13, 91, -91, 513, -513, 7513, -7513,
714+
17591, -17591, -4611686018427387904, 4611686018427387903,
715+
2**62, 2**63, 2**64, -(2**62), -(2**63), -(2**64)
716+
]
717+
718+
numbers.each do |number|
719+
assert_equal "[#{number}]", JSON.generate([number])
720+
end
721+
end
710722
end

0 commit comments

Comments
 (0)