LoginSignup
10
2

More than 1 year has passed since last update.

この記事はエイチーム引越し侍/エイチームコネクトの社員による、Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021 12日目の記事です。
本日は1歳児の育児でちょくちょく離席が多いパパエンジニア@lostfindが担当します。

誤った内容や日本語の指摘は大歓迎です。とても助かります。

はじめに😀

11月に「引越し侍 ネット見積もりサービス」をリリースしました。
こちらのBackend ビジネスロジックはGoによるClean Architectureの概念をベースで開発しました。

( 当サービスの詳細な技術の構成については1日目の記事と、2日目の記事をご覧ください )

Clean Architectureの特性上、構造体(モデル)が複数の層を渡って処理されるパターンが多いです。
その時に、引数としての構造体とそのスライスの型はほとんど構造体のポインタ型で扱っています。

ポインタ型だから値コピーされないので、早い、メモリ効率が良いと思い込んでいましたが、
「それ、本当か?」と思いましたので、軽く浅く検証してみました。

Benchmark⏱

go version 1.17.4

検証の準備

const sliceLen = 25
const maxNestedLevel = 4

type TestStr struct {
    ID     int
    Name   string
    Groups []string
    Steps  []string
    Child  *SubStr
}

type SubStr struct {
    ID      int
    SubName string
}

func NewTestStr() TestStr {
    rand.Seed(time.Now().UnixNano())
    id := rand.Intn(10000)
    name := fmt.Sprintf("TesterValueName_%d", id)
    child := SubStr{
        ID:      id + 1000,
        SubName: name,
    }

    return TestStr{
        ID:     id,
        Name:   name,
        Groups: []string{name, name, name, name, name, name, name},
        Steps:  []string{"init"},
    }
}

構造体のスライス🥒

リポジトリからデータを取得する時はだいたい構造体のスライスを返しているので、
その結果を持って各層で何かをする想定の検証用コードです。

func RecursionPointerSlice(input []*TestStr, lv int) []*TestStr {
    for i := range input {
        input[i].Steps = append(input[i].Steps, strconv.Itoa(lv))
    }

    nextLevel := lv + 1
    if nextLevel > maxNestedLevel {
        return input
    }

    return RecursionPointerSlice(input, nextLevel)
}

func RecursionValueSlice(input []TestStr, lv int) []TestStr {
    // 中身は上の関数と同じ
    ... 
}
func Benchmark_Pointer_NestedFunc_PointerSlice(b *testing.B) {
    input := PointerSlice()
    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        RecursionPointerSlice(input, 1)
    }
}

func Benchmark_Value_NestedFunc_ValueSlice(b *testing.B) {
    ...
}

結果

ベンチマークの結果は構造体のポインタではない方早かったです。差は10%程度。
ただ、sliceの内部ではポインタになっているので、何回ベンチマークして見たら両方のパフォーマンスの差はないようでした。

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz

Benchmark_Pointer_NestedFunc_PointerSlice-4       160000              7877 ns/op            8831 B/op          0 allocs/op
Benchmark_Value_NestedFunc_ValueSlice-4           160000              6938 ns/op            8831 B/op          0 allocs/op

構造体の単体🍅

構造体の単体で扱う時はどれぐらい差があるか確認してみました。
引数で受け取った構造体の中身でなにかをすることを想定してJustReturn()という関数を呼び出しています。

func RecursionPointer(input *TestStr, lv int) *TestStr {
    nextLevel := lv + 1
    JustReturn(input.Name)
    if nextLevel > maxNestedLevel {
        return input
    }

    return RecursionPointer(input, nextLevel)
}

func RecursionValue(input TestStr, lv int) TestStr {
    ...
}

//go:noinline
func JustReturn(name string) string {
    return name
}

JustReturn()関数はinline展開されて何も影響しなくなるので go:noinline というpragmaをつけます

cmd/compile go:noinline

結果

ポインタ型のほうが4倍ぐらい早い結果となりました。
テスト対象である構造体を大きくしたり、再帰をもっと深くすると、差はもっと広がりポインタ型の方が10倍ぐらい早い結果となりました。

Benchmark_Pointer_NestedFunc_Pointer-4          17000000                17.35 ns/op            0 B/op          0 allocs/op
Benchmark_Value_NestedFunc_Value-4              17000000                66.96 ns/op            0 B/op          0 allocs/op

まとめ

Slice、Mapのときはポインタ型にしなくても良い。 単体の構造体インスタンスを複数の関数やメソッドに引き渡すときはポインタ型が良い。

Immutabilityが重要なところではポインタ型は扱いに注意するべき。

これ以外にも、コードを色々変えながらベンチマークしてみましたが、もっと得られる情報はありませんでした。
構造体が小さい場合や、メソッド呼び出しのネストが深くない場合はポインタ型でなくても良いかと思います。

すこしの心残り

goのbenchmarkだけで判断しているため、偏ったまとめになってしまいました。
garbage collectorの動き方によっては実際に動くアプリケーションでは異なる結果になるかもしれないなと思いました。

明日 😉

明日は@kekiさんの記事です!お楽しみに :loudspeaker:

10
2
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
10
2