Go
golang
Go4Day 4

トリビア: Goの構造体int型のフィールド数4つまでなら値渡しとポインタ渡しに速度差はない(Go1.11にて確認)

この記事は Go4 Advent Calendar 2018 の4日目です。
3日目は、@andouf さんの 「ストリームの流れを図でコメントに残す」 でした。

はじめに

ちょっと下のコードを見てみてください。

type SameStruct struct {
   Some int
   Some02 int
}

func ValRetFunc() (SameStruct, error) {
  return SameStruct{/* */}, nil
}

func PointerRetFunc() (*SameStruct, error) {
  return &SameStruct{/* */}, nil
}

構造体の値を引き継ぐときに、値渡しポインタ渡し、どっちを使いますか?
 使うときの方針とか考えたことありますか?

他のエンジニアにお聞きすると、機械的にポインタ渡しをしている。という話を何度か聞きました。

確かにjs等の言語はオブジェクトは参照渡しですし、いちいち値を作るのは無駄な気がするので、ポインタ渡しで固定してしまう気持ちもわかります。

でもそれだったらGoの言語仕様としてなぜ値渡しがあるのでしょう?

それに、自分は保守性観点で極力値渡しをしたいです。nilの可能性がないことが関数のinterfaceから示せるからです。

実際調べてみると、Go Code Review Commentsにて、小さな構造体なら値渡しを推奨しています。

たかだか数バイトを節約するために引数にポインタを指定するのは辞めましょう。 もし関数が全体を通して引数 xを *x として呼び出しているなら、引数をポインタにするべきではありません。 --省略-- このアドバイスは、大きなStructや今後大きくなりそうなものには適用しません。

参考:https://gist.github.com/knsh14/0507b98c6b62959011ba9e4c310cd15d#pass-values

ですが 具体的にどのくらいの大きさの構造体 までなら値渡しで良いのかを示されていません。
これを検証してみます。

検証

構造体のメンバー数ごとにベンチマークを作成し実行結果を測定すれば簡単にわかりそうです。
以下はメンバー数2の場合のコード例です。


package main

import (
    "testing"
)

type ValueVsPointer02 struct {
    Field1 int
    Field2 int
}

func returnValue02(i ValueVsPointer02) ValueVsPointer02 {
    return i
}

func returnPointer02(i *ValueVsPointer02) *ValueVsPointer02 {
    return i
}

func BenchmarkUsePointerFieldCount02(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := returnPointer02(
            &ValueVsPointer02{
                Field1: 1,
                Field2: 2,
            },
        )
        if v.Field1 != 1 || v.Field2 != 2 {
            b.Fail()
        }
    }
}

func BenchmarkUseValueFieldCount02(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := returnValue02(
            ValueVsPointer02{
                Field1: 1,
                Field2: 2,
            },
        )
        if v.Field1 != 1 || v.Field2 != 2 {
            b.Fail()
        }
    }
}

Field数1から1つづつ増やし計測してみます。
githubにもコードを置いておきます。
https://github.com/m0a-mystudy/value_vs_pointer

そんなこんなで計測結果です。

$ go version
go version go1.11.1 darwin/amd64

$ go test -bench . -benchtime=10s
goos: darwin
goarch: amd64
pkg: github.com/m0a-mystudy/value_vs_pointer
BenchmarkUsePointerFieldCount01-8       10000000000          0.28 ns/op
BenchmarkUseValueFieldCount01-8         10000000000          0.28 ns/op
BenchmarkUsePointerFieldCount02-8       10000000000          0.28 ns/op
BenchmarkUseValueFieldCount02-8         10000000000          0.28 ns/op
BenchmarkUsePointerFieldCount03-8       10000000000          0.28 ns/op
BenchmarkUseValueFieldCount03-8         10000000000          0.28 ns/op
BenchmarkUsePointerFieldCount04-8       10000000000          0.28 ns/op
BenchmarkUseValueFieldCount04-8         10000000000          0.28 ns/op
BenchmarkUsePointerFieldCount05-8       10000000000          2.30 ns/op
BenchmarkUseValueFieldCount05-8         3000000000           4.74 ns/op
BenchmarkUsePointerFieldCount08-8       3000000000           4.18 ns/op
BenchmarkUseValueFieldCount08-8         2000000000           7.00 ns/op
PASS
ok      github.com/m0a-mystudy/value_vs_pointer 88.249s

つまり、

type ValueVsPointer04 struct {
    Field1 int
    Field2 int
    Field3 int
    Field4 int
}

上記構造体までなら値渡しとアドレス渡しに速度差はありませんでした。

こうして、この世にまた一つトリビアが生まれました。

Go構造体 int型のフィールド数4つまでなら値渡しとポインタ渡しに速度差はない(Go1.11にて確認)

でもまぁField数が5つでもそんなに大きな差があるわけでもないですし、あくまで目安といったところです。

小ネタですいません

参考:
https://qiita.com/marnie_ms4/items/7014563083ca1d824905