0
0

Sprintf関数は本当に遅いのか、検証してみた

Last updated at Posted at 2023-10-09

経緯

Goのプログラムで、文字列を結合する処理を行うために以下のようなコードを書いてプルリクを出したところ、

    id := "1000"
    url := fmt.Sprintf("https://example/path?id=%s", id)

以下のような指摘をもらった。

Sprintfは遅いので、文字列結合を使ってください

当時は、素直にurl = "https://example/path?id=" + idのように書いて終わったのだが、本当に遅いのか、実証できていなかったので確認する事にする。

検証環境

macOS Ventura 13.2.1

$ go version
go version go1.21.2 darwin/amd64

検証

Sprintfと、単純に文字列を結合する処理にかかる時間を比較するため、以下のメソッドを用意する。

func formatStringUsingUnion() {
	_ = "Hello" + "," + "world"
}

func formatStringUsingFmt() {
	_ = fmt.Sprintf("%s,%s", "Hello", "world")
}

そして、main関数でそれぞれのメソッドを実行し、かかった時間(ナノ秒数)を計測する

func main() {
    fmt.Println("formatStringUsingUnion")
    then := time.Now()
    formatStringUsingUnion()
    fmt.Println(time.Now().Sub(then).Nanoseconds())
}
func main() {
    fmt.Println("formatStringUsingFmt")
	then := time.Now()
	formatStringUsingFmt()
	fmt.Println(time.Now().Sub(then).Nanoseconds())
}

すると、以下のような結果が得られた。

% go run main.go
formatStringUsingUnion
124
% go run main.go
formatStringUsingFmt
2203

単純に文字列を結合するのに比較し、fmt.Sprintfは10倍近く時間がかかることが分かる。
また、runtime.ReadMemStats関数を用いても、消費されるメモリの量は2倍近くになっていることがわかった

なぜ、Sprintfは遅いのか

Sprintfの実装を見てみる。

// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...any) string {
	p := newPrinter()
	p.doPrintf(format, a)
	s := string(p.buf)
	p.free()
	return s
}

ここでは、ppポインタを生成している。

p := newPrinter()

そして、ここが異様に長い。

p.doPrintf(format, a)

func (p *pp) doPrintf(format string, a []any) {
...
}

doPrintfの中身は、長くなるので、ここに貼ることは省略するとして、
以下のような処理が行われている。

まず、入力されたフォーマット文字列の内容を解析し、処理している。
フォーマット文字として%s,%v,%dなどの他にも、
#や空白文字が渡る場合がある。
どういったフォーマットが指定されているのか、全て確認している。

そして、フォーマットに基づいて内部で保持しているbufferに1文字ずつ追記し、string(p.buf)の部分で文字列に変換されている。

Go Forumを探してみたところ、既に同じような指摘があった。

I was testing a simple function and noticed that the benchmarks for fmt.Sprintf seem much slower as opposed to just concatenating the strings.
訳) fmt.Sprintfのベンチマークが、単に文字列を連結するよりもずっと遅いことに気づいた。

デベロッパーの回答として、文字列フォーマットは遅いですとあった。
やはり、フォーマット文字列をパースするためにコストがかかっているらしい。

Formatting strings is slower. The arguments to Sprintf (or Printf, Fprintf, etc.) have to be wrapped into interface{}s, then put into an []interface{} slice, then the format string has to be parsed for formatting directives, an underlying buffer has to be created, and then the parsed format string is written into it

対応

Sprintfが遅いという事で、文字列を繋げる程度の処理であれば、基本は結合した方が良さそう。結合された後のフォーマットが決まっているのであれば、結合する処理をラップして専用のメソッドに切り出すなどをした方がいいと思う。また、スレッドの中で提案されている通り、strings.Joinを使うのも1つの策にはなると思う。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0