0

I compiled a go code with tinygo to WebAssembly and I don't understand why the function took 17 minutes to execute while the same function in JavaScript only took 4000 ms. what I'm doing wrong? also it is possible to modify the function in Go so I don't have to use "syscall/js"? I tried using function exports but couldn't return the array with different types to JavaScript.

Here is my Go code:

package main

import (
    "fmt"
    "math"
    "syscall/js"
)

type p struct {
    x float64
    y float64
}
type z struct {
    x float64
    y float64
}

func mandelbrotTinyGo(_ js.Value, args []js.Value) interface{} {

    maxIteration := args[0].Int()
    var newZ = z{0, 0}
    var newP = p{0, 0}
    n := 0
    cx := args[1].Float()
    cy := args[2].Float()
    d := 0.0

    for {
        newP = p{math.Pow(newZ.x, 2) - math.Pow(newZ.y, 2), 2 * newZ.x * newZ.y}
        newZ = z{newP.x + cx, newP.y + cy}
        d = 0.5 * (math.Pow(newZ.x, 2) + math.Pow(newZ.y, 2))
        n += 1
        if d >= 2 || maxIteration <= n {
            break
        }
    }

    arr := []interface{}{n, d <= 2}

    return arr

}

func main() {
    fmt.Println("TinyGo")

    js.Global().Set("mandelbrotTinyGo", js.FuncOf(mandelbrotTinyGo))

    <-make(chan bool)
}

Compiled with:

tinygo build -o tinygo.wasm -target wasm ./main.go

JavaScript code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="wasm_exec.js"></script>

    <script>

        const go = new Go();
        const WASM_URL = 'tinygo.wasm';

        var wasm;

        if ('instantiateStreaming' in WebAssembly) {
            WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
                wasm = obj.instance;
                go.run(wasm);
            })
        } else {
            fetch(WASM_URL).then(resp =>
                resp.arrayBuffer()
            ).then(bytes =>
                WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
                    wasm = obj.instance;
                    go.run(wasm);
                })
            )
        }

    </script>
</head>
<body>

<button id="btnDraw">Draw</button>
<canvas id="myCanvas" style="display: block; margin-left: auto;margin-right: auto;">
</canvas>

<script>

    const MAX_ITERATION = 80

    var canvas = document.getElementById('myCanvas')
    var ctx = canvas.getContext('2d')

    const WIDTH = window.innerWidth
    const HEIGHT = window.innerHeight
    ctx.canvas.width = WIDTH
    ctx.canvas.height = HEIGHT

    const REAL_SET = { start: -2, end: 1 }
    const IMAGINARY_SET = { start: -1, end: 1 }

    const colors = new Array(16).fill(0).map((_, i) => i === 0 ? '#000' : '#' + Math.random().toString(16).substr(2, 6))

    function draw() {
        for (let i = 0; i < WIDTH; i++) {
            for (let j = 0; j < HEIGHT; j++) {
                complex = {
                    x: REAL_SET.start + (i / WIDTH) * (REAL_SET.end - REAL_SET.start),
                    y: IMAGINARY_SET.start + (j / HEIGHT) * (IMAGINARY_SET.end - IMAGINARY_SET.start)
                }

                //Call the JS function
                //const [m, isMandelbrotSet] = mandelbrot(complex)

                //Call the WebAssembly/tinyGo function
                const [m, isMandelbrotSet] = mandelbrotTinyGo(MAX_ITERATION, complex.x, complex.y)
                ctx.fillStyle = colors[isMandelbrotSet ? 0 : (m % colors.length - 1) + 1]
                ctx.fillRect(i, j, 1, 1)
            }
        }
    }

    function mandelbrot(c) {
        let z = { x: 0, y: 0 }, n = 0, p, d;
        do {
            p = {
                x: Math.pow(z.x, 2) - Math.pow(z.y, 2),
                y: 2 * z.x * z.y
            }
            z = {
                x: p.x + c.x,
                y: p.y + c.y
            }
            d = Math.sqrt(Math.pow(z.x, 2) + Math.pow(z.y, 2))
            n += 1
        } while (d <= 2 && n < MAX_ITERATION)
        return [n, d <= 2]
    }

    function start(){
        let startTime = performance.now()
        draw()
        let endTime = performance.now()
        console.log(`Call to doSomething took ${endTime - startTime} milliseconds`)
    }

    myButton = document.getElementById("btnDraw");

    myButton.addEventListener("click", function() {
        start();
    });
</script>

</body>
</html>

The file wasm_exec.js must to be copied from your ..\tinygo\0.25.0\targets\ directory

tinygo version 0.25.0 windows/amd64 (using go version go1.19.1 and LLVM version 14.0.0)

insurg3nt3
  • 63
  • 4

2 Answers2

1

I had the same issue with TinyGo. My solution was to use the native 'go build' instead of TinyGo. That produces a faster Wasm module but with a larger size.

Unfortunately, in my case, the performance it's not yet better compared with native Javascript. It might be missing optimizations when compiling to Wasm since running the go program natively is much faster than the Native Javascript/Nodejs version.

1

Did you try -opt=2 ? It's fairly important.

See https://tinygo.org/docs/reference/usage/important-options

dan kegel
  • 11
  • 1