7

[ANSWER] Go doesn't buffer stdout. Switching to a buffered version and manually flushing brings it much closer to what you would expect. Avoiding fmt makes it run as fast as you like.

I'm trying to write the FizzBuzz program in Go.

func main() {
  for i := 1; i <= 1000000; i++ {
    fmt.Println(fizzbuzz(i))
  }
}

func fizzbuzz(n int) string {
  fizzy := n%3 == 0
  buzzy := n%5 == 0

  switch {
  case fizzy && buzzy:
    return "FizzBuzz"
  case fizzy:
    return "Fizz"
  case buzzy:
    return "Buzz"
  default:
    return fmt.Sprint(n)
  }
}

When I run it for numbers from 1 to a million it takes just under a second to complete. When I write the equivalent program in C, Rust, Haskell or Python it takes anywhere from half a second (Python) to zero seconds (Rust and Haskell).

Is this to be expected, or am I missing some Go-fu? Why does the go seem slower than the other languages?

[EDIT]

Running with the profiler as suggested by Robert Harvey.

It looks like 100% of the time is spent in fmt.(*fmt).fmt_complex, which I'm guessing is related to the Println(?). Also tried the program with strconv.Itoa instead of the fmt.Sprint and I get the slight performance increase (~0.2s) but the same basic results.

Is it the printing that's slow and if so why?

[EDIT]

For jgritty the equivalent Python program and timings. I'm interested in why the printing is slower? Is go doing something behind the scenes I'm not aware of?

$ cat fizzbuzz.py
def fizzbuzz(n):
    fizzy = n%3 == 0
    buzzy = n%5 == 0

    if fizzy and buzzy:
        return "FizzBuzz"
    elif fizzy:
        return "Fizz"
    elif buzzy:
        return "Buzz"
    else:
        return ("%u" % n)

def main():
    for i in range(1, 10**6):
        print(fizzbuzz(i))

main()
$ time pypy3 fizzbuzz.py >/dev/null

real    0m0.579s
user    0m0.545s
sys     0m0.030s
cdlane
  • 40,441
  • 5
  • 32
  • 81
Joseph
  • 73
  • 4

1 Answers1

6

The standard output is buffered in Python and C, but not Go. Buffer the output for an apples to apples comparison. This almost cut the time in half on my laptop.

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    w := bufio.NewWriter(os.Stdout)
    for i := 1; i <= 1000000; i++ {
         fmt.Fprintln(w, fizzbuzz(i))
    }
    w.Flush()
}

Eliminate the use of the fmt package for another improvement:

package main

import (
    "bufio"
    "os"
    "strconv"
)

func main() {
    w := bufio.NewWriter(os.Stdout)
    for i := 1; i <= 1000000; i++ {
        w.WriteString(fizzbuzz(i))
        w.WriteString("\n")
    }
    w.Flush()
}

func fizzbuzz(n int) string {
    fizzy := n%3 == 0
    buzzy := n%5 == 0

    switch {
    case fizzy && buzzy:
        return "FizzBuzz"
    case fizzy:
        return "Fizz"
    case buzzy:
        return "Buzz"
    default:
        return strconv.Itoa(n)
    }
}
  • I assume you mean fmt.Fprintln instead of Println. – Joseph Oct 01 '14 at 20:46
  • That seems to get me a massive performance improvement! Four times speed up, and we're down to a quarter of a second. – Joseph Oct 01 '14 at 20:48
  • 2
    Interesting that Go decided not to buffer stdout by default: what's the rationale? – Joseph Oct 01 '14 at 20:49
  • This runs amazingly fast! `real 0m0.047s` `user 0m0.043s` `sys 0m0.003s` – jgritty Oct 01 '14 at 20:50
  • @Joseph: its biggest benefit for me is that, by default, you know data was written by the time `Write` returns--handy to avoid "hangs" in stdout interaction, logging, etc., crucial if order or timing of writes matter for your reliability. Buffering is easy to layer on when write-when-you-`Write` is costly or not what you want. – twotwotwo Oct 01 '14 at 21:11
  • 1
    @Joseph (On a less useful note, such microbenchmark, much hyper-tune, wow. ;) ) – twotwotwo Oct 01 '14 at 21:28
  • @twotwotwo: Yes I completely agree its handy, its just surprising when you come from C (and Haskell, and most other languages I guess) where we normally don't care about the time write returns (and use the unbuffered stderr for logging). Just a (completely sane) quirk of Go I guess. – Joseph Oct 01 '14 at 22:09
  • @twotwotwo and such a microbench mark! To quote Knuth: "about 97% of the time: premature optimization is the root of all evil but we shouldn't ignore opportunities in the remaining 3%". To be perfectly honest I'm playing with learning Rust and Go: it just seemed odd that the code behaved noticeably differently for such a simple program. – Joseph Oct 01 '14 at 22:13
  • @Joseph: Fair question: Why not buffering by default? It's in the same category as the question, why go doesn't have (yet) tail call optimisations: https://www.goinggo.net/2013/09/recursion-and-tail-calls-in-go_26.html – Christof Kälin Nov 26 '17 at 12:19