Skip to content

Commit 1efbe5f

Browse files
authored
Merge pull request #5969 from Kelimion/faster_big_itoa
Faster `big.itoa`.
2 parents 78d8059 + eff32e1 commit 1efbe5f

File tree

3 files changed

+140
-11
lines changed

3 files changed

+140
-11
lines changed

core/math/big/common.odin

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,15 @@ when MATH_BIG_FORCE_64_BIT || (!MATH_BIG_FORCE_32_BIT && size_of(rawptr) == 8) {
223223
*/
224224
DIGIT :: distinct u64
225225
_WORD :: distinct u128
226+
// Base 10 extraction constants
227+
ITOA_DIVISOR :: DIGIT(1_000_000_000_000_000_000)
228+
ITOA_COUNT :: 18
226229
} else {
227230
DIGIT :: distinct u32
228231
_WORD :: distinct u64
232+
// Base 10 extraction constants
233+
ITOA_DIVISOR :: DIGIT(100_000_000)
234+
ITOA_COUNT :: 8
229235
}
230236
#assert(size_of(_WORD) == 2 * size_of(DIGIT))
231237

core/math/big/radix.odin

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,15 @@ int_itoa_raw :: proc(a: ^Int, radix: i8, buffer: []u8, size := int(-1), zero_ter
185185
/*
186186
Fast path for radixes that are a power of two.
187187
*/
188+
count := count_bits(a) or_return
189+
188190
if is_power_of_two(int(radix)) {
189191
if zero_terminate {
190192
available -= 1
191193
buffer[available] = 0
192194
}
193195

194-
shift, count: int
195-
// mask := _WORD(radix - 1);
196-
shift, err = log(DIGIT(radix), 2)
197-
count, err = count_bits(a)
196+
shift := log(DIGIT(radix), 2) or_return
198197
digit: _WORD
199198

200199
for offset := 0; offset < count; offset += shift {
@@ -224,7 +223,17 @@ int_itoa_raw :: proc(a: ^Int, radix: i8, buffer: []u8, size := int(-1), zero_ter
224223
return written, nil
225224
}
226225

227-
return _itoa_raw_full(a, radix, buffer, zero_terminate)
226+
// NOTE(Jeroen): The new method is faster for an `Int` up to ~32768 bits in size with optimizations.
227+
// At `.None` or `.Minimal`, it appears to always be faster.
228+
// If we optimize `itoa` further, this needs to be evaluated.
229+
itoa_method := _itoa_raw_full
230+
231+
when ODIN_OPTIMIZATION_MODE >= .Size {
232+
if count >= 32768 {
233+
itoa_method = _itoa_raw_old
234+
}
235+
}
236+
return itoa_method(a, radix, buffer, zero_terminate)
228237
}
229238

230239
itoa :: proc{int_itoa_string, int_itoa_raw}
@@ -601,14 +610,89 @@ RADIX_TABLE_REVERSE_SIZE :: 80
601610
Stores a bignum as a ASCII string in a given radix (2..64)
602611
The buffer must be appropriately sized. This routine doesn't check.
603612
*/
613+
604614
_itoa_raw_full :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false, allocator := context.allocator) -> (written: int, err: Error) {
605615
assert_if_nil(a)
606616
context.allocator = allocator
607617

608-
temp, denominator := &Int{}, &Int{}
618+
// Calculate largest radix^n that fits within _DIGIT_BITS
619+
divisor := ITOA_DIVISOR
620+
digit_count := ITOA_COUNT
621+
_radix := DIGIT(radix)
622+
623+
if radix != 10 {
624+
i := _WORD(1)
625+
digit_count = -1
626+
for i < _WORD(1 << _DIGIT_BITS) {
627+
divisor = DIGIT(i)
628+
i *= _WORD(radix)
629+
digit_count += 1
630+
}
631+
}
609632

610-
internal_copy(temp, a) or_return
611-
internal_set(denominator, radix) or_return
633+
temp := &Int{}
634+
internal_copy(temp, a) or_return
635+
defer internal_destroy(temp)
636+
637+
available := len(buffer)
638+
if zero_terminate {
639+
available -= 1
640+
buffer[available] = 0
641+
}
642+
643+
if a.sign == .Negative {
644+
temp.sign = .Zero_or_Positive
645+
}
646+
647+
remainder: DIGIT
648+
for {
649+
if remainder, err = internal_divmod(temp, temp, divisor); err != nil {
650+
return len(buffer) - available, err
651+
}
652+
653+
count := digit_count
654+
for available > 0 && count > 0 {
655+
available -= 1
656+
buffer[available] = RADIX_TABLE[remainder % _radix]
657+
remainder /= _radix
658+
count -= 1
659+
}
660+
661+
if temp.used == 0 {
662+
break
663+
}
664+
}
665+
666+
// Remove leading zero if we ended up with one.
667+
if buffer[available] == '0' {
668+
available += 1
669+
}
670+
671+
if a.sign == .Negative {
672+
available -= 1
673+
buffer[available] = '-'
674+
}
675+
676+
/*
677+
If we overestimated the size, we need to move the buffer left.
678+
*/
679+
written = len(buffer) - available
680+
if written < len(buffer) {
681+
diff := len(buffer) - written
682+
mem.copy(&buffer[0], &buffer[diff], written)
683+
}
684+
return written, nil
685+
}
686+
687+
// Old internal digit extraction procedure.
688+
// We're keeping this around as ground truth for the tests.
689+
_itoa_raw_old :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false, allocator := context.allocator) -> (written: int, err: Error) {
690+
assert_if_nil(a)
691+
context.allocator = allocator
692+
693+
temp := &Int{}
694+
internal_copy(temp, a) or_return
695+
defer internal_destroy(temp)
612696

613697
available := len(buffer)
614698
if zero_terminate {
@@ -623,7 +707,6 @@ _itoa_raw_full :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false
623707
remainder: DIGIT
624708
for {
625709
if remainder, err = #force_inline internal_divmod(temp, temp, DIGIT(radix)); err != nil {
626-
internal_destroy(temp, denominator)
627710
return len(buffer) - available, err
628711
}
629712
available -= 1
@@ -638,8 +721,6 @@ _itoa_raw_full :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false
638721
buffer[available] = '-'
639722
}
640723

641-
internal_destroy(temp, denominator)
642-
643724
/*
644725
If we overestimated the size, we need to move the buffer left.
645726
*/

tests/core/math/big/test_core_math_big.odin

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,46 @@ atoi :: proc(t: ^testing.T, i: ^big.Int, a: string, loc := #caller_location) ->
287287
err := big.atoi(i, a, 16)
288288
testing.expect(t, err == nil, loc=loc)
289289
return err == nil
290+
}
291+
292+
@(test)
293+
test_itoa :: proc(t: ^testing.T) {
294+
a := &big.Int{}
295+
big.random(a, 2048)
296+
defer big.destroy(a)
297+
298+
for radix in 2..=64 {
299+
if big.is_power_of_two(radix) {
300+
// Powers of two are trivial, and are handled before `_itoa_raw_*` is called.
301+
continue
302+
}
303+
304+
size, _ := big.radix_size(a, i8(radix), false)
305+
buffer_old := make([]u8, size)
306+
defer delete(buffer_old)
307+
buffer_new := make([]u8, size)
308+
defer delete(buffer_new)
309+
310+
written_old, _ := big._itoa_raw_old (a, i8(radix), buffer_old, false)
311+
written_new, _ := big._itoa_raw_full(a, i8(radix), buffer_new, false)
312+
313+
str_old := string(buffer_old[:written_old])
314+
str_new := string(buffer_new[:written_new])
315+
testing.expect_value(t, str_new, str_old)
316+
}
317+
318+
// Also test a number with a large number of zeroes
319+
big.set(a, "2970714761494550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000802525522395693895558562897961119110387707542077460459880227570865486047631557732177235787527971863645406120285117781450154113859156752194121206131440514109132606823127467068869589613665129498148285292867292641704871893467328665051712596763187306247339023362481")
320+
size, _ := big.radix_size(a, 10, false)
321+
buffer_old := make([]u8, size)
322+
defer delete(buffer_old)
323+
buffer_new := make([]u8, size)
324+
defer delete(buffer_new)
325+
326+
written_old, _ := big._itoa_raw_old (a, 10, buffer_old, false)
327+
written_new, _ := big._itoa_raw_full(a, 10, buffer_new, false)
328+
329+
str_old := string(buffer_old[:written_old])
330+
str_new := string(buffer_new[:written_new])
331+
testing.expect_value(t, str_new, str_old)
290332
}

0 commit comments

Comments
 (0)