Skip to content

math.big: move from u32 to u60 digits - #25018

Merged
spytheman merged 4 commits into
vlang:masterfrom
kbkpbot:math-big-use-60-bit-u64
Aug 14, 2025
Merged

spytheman merged 4 commits into
vlang:masterfrom
kbkpbot:math-big-use-60-bit-u64

Conversation

@kbkpbot

@kbkpbot kbkpbot commented Aug 1, 2025 •

Copy link
Copy Markdown
Contributor

This PR:

  1. use u64(60bits) represent Integer, instead of u32; (This can be changed in integer.v digit_bits)
  2. add a simple benchmark in bench.

This will use 64bit operations and it may improve performance.

TODO: Further optimization may focus on reducing memory allocation.

@huly-for-github

Copy link
Copy Markdown

Connected to Huly®: V_0.6-23535

Comment thread vlib/math/big/special_array_ops.v Outdated
Comment thread vlib/math/big/special_array_ops.v Outdated
Comment thread vlib/math/big/special_array_ops.v Outdated
Comment thread vlib/math/big/special_array_ops.v Outdated
@kbkpbot

kbkpbot commented Aug 1, 2025

Copy link
Copy Markdown
Contributor Author

I will move some functions to math.bits:
mul_64
mul_add_64
div_64

@spytheman

Copy link
Copy Markdown
Contributor

I will move some functions to math.bits: mul_64, mul_add_64, div_64

No, lets keep them in math.big for now. Those functions have inline assembly, and math.bits is used by strconv which in turn is used by builtin, which has the potential to complicate bootstrapping.
It can be done in another dedicated PR.

@spytheman

Copy link
Copy Markdown
Contributor

imho focus on fixing the failing tests

@tankf33der

Copy link
Copy Markdown
Contributor

I already have math.big ported to u64, 99% done.
Probably because it was u64, not u60, I didn't notice any performance improvement even though digits array became twice shorter. It was fun. Let me know when the migration is ready and I'll also test it for performance and correctness with my comprehensive tests. I have tests for the entire math.big.

@spytheman

Copy link
Copy Markdown
Contributor

@tankf33der can you please check the performance and correctness with this PR too?

@tankf33der

Copy link
Copy Markdown
Contributor

Working. This will take time. Wait for me.

@spytheman

Copy link
Copy Markdown
Contributor

Working. This will take time. Wait for me.

Thank you 🙇🏻 .

@spytheman

Copy link
Copy Markdown
Contributor

For me, the version from this PR is faster, except for the second a.bit_len line:

#0 12:27:24 ^ master ~/v>./vold -prod run /tmp/bench_big_dividing_big_numbers_big_pow.v
 SPENT  1820.568 ms in math.big> n: 2500000 | base.bit_len: 8304817
 SPENT     0.665 ms in math.big> a.bit_len: 8304821 | b.bit_len: 8304819
 SPENT     3.620 ms in math.big> c: 3 | c.bit_len(): 2
#0 12:27:36 ^ master ~/v>./vold -prod run /tmp/bench_big_dividing_big_numbers_big_pow.v
 SPENT  1836.690 ms in math.big> n: 2500000 | base.bit_len: 8304817
 SPENT     0.756 ms in math.big> a.bit_len: 8304821 | b.bit_len: 8304819
 SPENT     3.648 ms in math.big> c: 3 | c.bit_len(): 2
#0 12:27:45 ^ master ~/v>./vold -prod run /tmp/bench_big_dividing_big_numbers_big_pow.v
 SPENT  1831.143 ms in math.big> n: 2500000 | base.bit_len: 8304817
 SPENT     0.665 ms in math.big> a.bit_len: 8304821 | b.bit_len: 8304819
 SPENT     3.622 ms in math.big> c: 3 | c.bit_len(): 2
#0 12:27:53 ^ master ~/v>git switch -
Switched to branch 'math-big-use-60-bit-u64'
#0 12:27:55 ^ math-big-use-60-bit-u64 ~/v>./vnew -prod run /tmp/bench_big_dividing_big_numbers_big_pow.v
 SPENT  1168.851 ms in math.big> n: 2500000 | base.bit_len: 8304817
 SPENT     1.095 ms in math.big> a.bit_len: 8304821 | b.bit_len: 8304819
 SPENT     1.953 ms in math.big> c: 3 | c.bit_len(): 2
#0 12:28:04 ^ math-big-use-60-bit-u64 ~/v>./vnew -prod run /tmp/bench_big_dividing_big_numbers_big_pow.v
 SPENT  1182.893 ms in math.big> n: 2500000 | base.bit_len: 8304817
 SPENT     1.097 ms in math.big> a.bit_len: 8304821 | b.bit_len: 8304819
 SPENT     1.948 ms in math.big> c: 3 | c.bit_len(): 2
#0 12:28:14 ^ math-big-use-60-bit-u64 ~/v>./vnew -prod run /tmp/bench_big_dividing_big_numbers_big_pow.v
 SPENT  1171.803 ms in math.big> n: 2500000 | base.bit_len: 8304817
 SPENT     1.109 ms in math.big> a.bit_len: 8304821 | b.bit_len: 8304819
 SPENT     1.958 ms in math.big> c: 3 | c.bit_len(): 2
#0 12:28:27 ^ math-big-use-60-bit-u64 ~/v>

@spytheman

Copy link
Copy Markdown
Contributor

On my i3-3225, on Ubuntu 20.04, with gcc-10, vlib/math/big/large_number_power_and_string_conversion_test.v also seems several times slower on the PR, compared to master - it finished in 211633 µs vs 41936 µs on master:
image

Note that the test file itself is not changed between the two branches.

@spytheman

Copy link
Copy Markdown
Contributor

There is slowdown for tcc for vlib/math/big/big_test.v too:
457.700 ms vlib/math/big/big_test.v on master
and
752.111 ms vlib/math/big/big_test.v on this PR.

image

@spytheman

Copy link
Copy Markdown
Contributor

The compile time has reduced though, interesting.

@tankf33der

Copy link
Copy Markdown
Contributor

The implementation works correctly. This is good.
The only thing I see is a performance drop in integer_from_string() and str().

Run this code to have fun on master and PR branches:

import math
import math.big
import time

fn main() {
        mut t1 := time.now()
        mut x := u32(math.pow(3, 2))
        x = u32(math.pow(4, x))
        mut y := big.integer_from_int(5)
        b := y.pow(x)
        a := b * b
        println('prepare: ${time.since(t1)}')

        println('a -> ${a.str().len} digits')
        println('b -> ${b.str().len} digits')

        s1 := a.str()
        t1 = time.now()
        _ := big.integer_from_string(s1)!
        println('from_string(): ${time.since(t1)}')

        t1 = time.now()
        _ := a.str()
        println('str(): ${time.since(t1)}')

        mut iterations := 100
        println('loop: ${iterations}')

        t1 = time.now()
        for _ in 0 .. iterations {
                _ := a + b
        }
        println('+: ${time.since(t1).nanoseconds() / iterations} ns/op')

        t1 = time.now()
        for _ in 0 .. iterations {
                _ := a - b
        }
        println('-: ${time.since(t1).nanoseconds() / iterations} ns/op')

        t1 = time.now()
        for _ in 0 .. iterations {
                _ := a * b
        }
        println('*: ${time.since(t1).nanoseconds() / iterations} ns/op')

        iterations = 1 // XXX, 1 iteration
        t1 = time.now()
        for _ in 0 .. iterations {
                _ := a / b
        }
        println('/: ${time.since(t1).nanoseconds() / iterations} ns/op')
}

I suggest not looking at division performance for now because my Knuth implementation is waiting its turn to create a PR.

@tankf33der

Copy link
Copy Markdown
Contributor

Here's the Golang performance with the same numbers.
The figures show what we need to aim for.

package main

import (
        "fmt"
        "math"
        "math/big"
        "time"
)

func main() {
        t1 := time.Now()
        x := uint32(math.Pow(3, 2))
        x = uint32(math.Pow(4, float64(x)))
        y := big.NewInt(5)
        b := new(big.Int).Exp(y, big.NewInt(int64(x)), nil)
        a := new(big.Int).Mul(b, b)
        fmt.Printf("prepare: %v\n", time.Since(t1))
        fmt.Printf("a -> %d digits\n", len(a.String()))
        fmt.Printf("b -> %d digits\n", len(b.String()))
        s1 := a.String()
        t1 = time.Now()
        _, ok := new(big.Int).SetString(s1, 10)
        if !ok {
                panic("failed to parse string")
        }
        fmt.Printf("from_string(): %v\n", time.Since(t1))
        t1 = time.Now()
        _ = a.String()
        fmt.Printf("str(): %v\n", time.Since(t1))
        iterations := 100
        fmt.Printf("loop: %d\n", iterations)
        t1 = time.Now()
        for i := 0; i < iterations; i++ {
                _ = new(big.Int).Add(a, b)
        }
        fmt.Printf("+: %d ns/op\n", time.Since(t1).Nanoseconds()/int64(iterations))
        t1 = time.Now()
        for i := 0; i < iterations; i++ {
                _ = new(big.Int).Sub(a, b)
        }
        fmt.Printf("-: %d ns/op\n", time.Since(t1).Nanoseconds()/int64(iterations))
        t1 = time.Now()
        for i := 0; i < iterations; i++ {
                _ = new(big.Int).Mul(a, b)
        }
        fmt.Printf("*: %d ns/op\n", time.Since(t1).Nanoseconds()/int64(iterations))
        iterations = 1 // XXX, 1 iteration
        t1 = time.Now()
        for i := 0; i < iterations; i++ {
                _ = new(big.Int).Div(a, b)
        }
        fmt.Printf("/: %d ns/op\n", time.Since(t1).Nanoseconds()/int64(iterations))
}

@JalonSolov

Copy link
Copy Markdown
Collaborator

Have you tried comparing to your u64 implementation? :-)

@tankf33der

Copy link
Copy Markdown
Contributor

Have you tried comparing to your u64 implementation? :-)

It is definitely worse. Cannot be compared. @kbkpbot implementation is very successful, we need to advance it further and rely on it. This is a big step step forward.

@spytheman
spytheman merged commit a1b131c into vlang:master Aug 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants