Skip to content

Commit 1ea5990

Browse files
committed
Speed up big.itoa
Extract 18 (64-bit) or 8 (32-bit) digits per big division. This gives a 2.5x speedup for a 1024-bit bigint.
1 parent 78d8059 commit 1ea5990

File tree

3 files changed

+111
-6
lines changed

3 files changed

+111
-6
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: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,89 @@ RADIX_TABLE_REVERSE_SIZE :: 80
601601
Stores a bignum as a ASCII string in a given radix (2..64)
602602
The buffer must be appropriately sized. This routine doesn't check.
603603
*/
604+
604605
_itoa_raw_full :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false, allocator := context.allocator) -> (written: int, err: Error) {
605606
assert_if_nil(a)
606607
context.allocator = allocator
607608

608-
temp, denominator := &Int{}, &Int{}
609+
// Calculate largest radix^n that fits within _DIGIT_BITS
610+
divisor := ITOA_DIVISOR
611+
digit_count := ITOA_COUNT
612+
_radix := DIGIT(radix)
613+
614+
if radix != 10 {
615+
i := _WORD(1)
616+
digit_count = -1
617+
for i < _WORD(1 << _DIGIT_BITS) {
618+
divisor = DIGIT(i)
619+
i *= _WORD(radix)
620+
digit_count += 1
621+
}
622+
}
609623

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

613688
available := len(buffer)
614689
if zero_terminate {
@@ -623,7 +698,6 @@ _itoa_raw_full :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false
623698
remainder: DIGIT
624699
for {
625700
if remainder, err = #force_inline internal_divmod(temp, temp, DIGIT(radix)); err != nil {
626-
internal_destroy(temp, denominator)
627701
return len(buffer) - available, err
628702
}
629703
available -= 1
@@ -638,8 +712,6 @@ _itoa_raw_full :: proc(a: ^Int, radix: i8, buffer: []u8, zero_terminate := false
638712
buffer[available] = '-'
639713
}
640714

641-
internal_destroy(temp, denominator)
642-
643715
/*
644716
If we overestimated the size, we need to move the buffer left.
645717
*/

tests/core/math/big/test_core_math_big.odin

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,31 @@ 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+
}
290317
}

0 commit comments

Comments
 (0)