Stream: contributing

Topic: Nea benchmarks in go (go help wanted)


view this post on Zulip Folkert de Vries (Jan 17 2024 at 10:22):

this may seem off-topic but it's relevant: I'm trying to write benchmarks in go to compare performance to nea (our roc webserver). I don't know any go, so I have no idea what I'm doing, and could use some help here

the server below should just serve the string "hello world!". But curl does not actually find that

> curl -i 127.0.0.1:8080
HTTP/1.1 200 OK
Date: Wed, 17 Jan 2024 10:20:01 GMT
Content-Length: 0

Ultimately I want to convert these benchmarks to use go. It is important that there is just one thread doing the listening on a socket, and then work gets distributed to N worker threads that process the request and send a response.

https://github.com/tweedegolf/nea/tree/main/benchmarks/roc-nea

Help would be most welcome!

package main

import (
    "fmt"
    "net/http"
)

import _ "embed"

var workerCount = 2
var workerPool chan struct{}

func main() {
    // Create a buffered channel to limit the number of concurrent worker threads
    workerPool = make(chan struct{}, workerCount)

    // Handle requests using a single IO thread
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // Acquire a worker thread from the pool
        workerPool <- struct{}{}

        // Handle the request in a goroutine
        go func() {
            // Ensure the worker is released back to the pool when done
            defer func() {
                <-workerPool
            }()

            // Handle the actual request (in this case, just returning "Hello, World!")
            handleRequest(w, r)
        }()
    })

    // Start the web server on port 8080
    fmt.Println("Server listening on :8080")
    http.ListenAndServe(":8080", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    // Simulate some work being done by the worker
    fmt.Println("Processing request...")

    // Set the Content-Type header to plain text
    w.Header().Set("Content-Type", "text/plain")

    // Get the response string
    response := "Hello, World!"

    // Set the Content-Length header
    w.Header().Set("Content-Length", fmt.Sprint(len(response)))

    // Write the response body
    fmt.Fprint(w, response)
}

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 15:41):

Not a solution, will need to play with the code later cause that looks like it should work. Anyway, general questions:
Do you want to limit go workers in that way? Goroutines are meant to be exceptionally cheap and should essentially be treated like async contexts. So they generally shouldn't be limited like regular threads.

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 15:41):

Go will automatically limit the number of actual threads

view this post on Zulip Oskar Hahn (Jan 17 2024 at 16:49):

There is a bug in the code. The go call in the callback function does not sync. So the callback returns before the inner go function runs. This means that the function handleRequest gets called after the request handler is finished. In this case, the variables r and w are not valid anymore. You cannot write "hello world" to the client, after the connection is closed.

Go starts a separate gorotine for any request anyway. So there is no need to start another one. To fix your code, you can just remove the line "go func()"

I also would not use a Worker Pool.

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:10):

Cool, I guessed something along those lines but haven't used go in a long while so didn't remember for sure.

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:11):

I assume for this work, probably just want to limit the number of system threads go uses with GOMAXPROCS

view this post on Zulip Folkert de Vries (Jan 17 2024 at 17:13):

I need a fair comparison. So in rust/tokio we have one "thread" that listens for requests on the socket, and N others that process requests. I just need to replicate that

view this post on Zulip Folkert de Vries (Jan 17 2024 at 17:13):

you can have many concurrent OS threads all listening to the same socket, and that would be faster but also not a fair comparison

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:13):

That said it just limits live threads running go code. It doesn't limit the number of threads waiting on system calls

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:16):

Is that a system thread limit or an async process limit?

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:18):

Looking at the go source. The server is single threaded dispatch that spawns a goroutine after accepting a request.

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:20):

If you want to restrict the number of goroutines, I guess you can just use the worker pool without a second go func call.

view this post on Zulip Brendan Hansknecht (Jan 17 2024 at 17:21):

Though go will end up generating a metric crap ton of waiting go routines and that will probably have really bad characteristics. Cause it will still accept every connection and spawn a go thread for it.

view this post on Zulip Folkert de Vries (Mar 04 2024 at 15:37):

finally coming back to this, thanks for the help earlier!

a quick thing I want to check, does this actually allocate the memory?

    // Allocate an array of N bytes using make
    byteArray := make([]byte, capacity)

Even if so I'd guess it doesn't initialize it? I want something like let v = vec![0xAA; capacity]; but in go

view this post on Zulip Folkert de Vries (Mar 04 2024 at 15:44):

fingers crossed this is turned into a memset?

        // Allocate an array of N bytes using make
        byteArray := make([]byte, capacity)

        for i := range byteArray {
            byteArray[i] = 0xAA
        }

view this post on Zulip Hristo (Mar 04 2024 at 16:50):

As far as I understand, byteArray := make([]byte, capacity) on its own does initialise the values of the underlying array (in addition to allocating memory) as well, as per this Go playground session.

Further background reading:

Update: I see now what your intention was in your code. Yes, as far as I'm aware, the only way to initialise to non-default values is the way you specified here with the for loop.

view this post on Zulip Folkert de Vries (Mar 04 2024 at 17:00):

cool! there is special hardware support for initializing to zero so that's why I want a different value. But it turns out to not matter too much anyway

view this post on Zulip Folkert de Vries (Mar 04 2024 at 17:01):

in that zero vs 0xAA does not make a big performance difference

view this post on Zulip Folkert de Vries (Mar 04 2024 at 17:11):

probably the final question: does this look reasonable

func handleRequest(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading request body", http.StatusInternalServerError)
        return
    }

    pathString, err := generateSVGPath(string(body))
    if err != nil {
        http.Error(w, "Error processing request body", http.StatusBadRequest)
        return
    }

    response := fmt.Sprintf(`<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
    <path d="%s" stroke="black" fill="transparent"/>
</svg>
`, pathString)

    _, _ = w.Write([]byte(response))
}

func generateSVGPath(requestBody string) (string, error) {
    lines := strings.Split(requestBody, "\n")

    var pathString strings.Builder
    pathString.WriteString("M 0 0 L")

    for _, line := range lines {
        parts := strings.SplitN(line, ", ", 2)
        if len(parts) != 2 {
            continue
        }

        x, errX := strconv.Atoi(parts[0])
        y, errY := strconv.Atoi(parts[1])

        if errX != nil || errY != nil {
            return "", fmt.Errorf("invalid input format")
        }

        _, _ = fmt.Fprintf(&pathString, " %d %d", x, y)
    }

    return pathString.String(), nil
}

in particular from a performance perspective?


Last updated: Jul 06 2025 at 12:14 UTC