doridoridoriand’s diary

主に技術的なことを書いていく予定(たぶん)

任意の型を引数に取り、そのフィールドの値を集計して返す

業務で利用しようとし、実装したのですがPerformance Issueにより不使用にしたので供養がてらブログネタとします。

type Fruits struct {
    Apple      float32
    Banana     float32
    Orange     float32
    Strawberry float32
    Raspberry  float32
    Kiwi       float32
    Coconut    float32
    Papaya     float32
    Grapefruit float32
    Avocado    float32
}

のような構造体があるとします。float32には価格が格納されています。また、この構造体は追加・削除がそこそこ発生します。 この時Fruits全体の合計値を知りたいと思った時、愚直に足し算を実装しても良いのですが、構造体の追加・削除が発生した時に実装の修正が必要になります。

これは少々煩雑なので、Fieldの数に依存せずに合計を計算出来ないかと考え、Reflectionを利用してFieldのValueを取得する関数を作ってみました。 (結構Copilotが作ってくれた)

func FruitsAllWithType(s interface{}) []float32 {
    v := reflect.ValueOf(s)
    typeOfS := v.Type()
    scores := make([]float32, v.NumField())
    for i := 0; i < v.NumField(); i++ {
        fieldName := typeOfS.Field(i).Name
        fieldValue := v.FieldByName(fieldName).Interface()
        if value, ok := fieldValue.(float32); ok {
            scores = append(scores, value)
        }
    }
    return scores
}

任意のstruct sをfor文でひたすら回して、逐次Fileld名を取得し、そのField名でValueをInterfaceとして取り出し、型がfloat32であればsliceに追加するという実装です。 以下で試すことができます go.dev

この実装は前述の通りPerformance Issueがあるため不使用としました(まあそもそも map[string]float32 のほうが利便性含め良くないかってこともあったんですが)

先ほどの関数をgo標準のtesting packageにあるbenckmarkを利用して、計算時間を計測しました。 10種類のFields及びmap[string]float32の足し合わせを100万回ループさせた時の測定結果です

goos: darwin
goarch: arm64

BenchmarkCalCAllFruitPrice1-12          1000000000               1.056 ns/op
BenchmarkCalCAllFruitPrice2-12          1000000000               0.08979 ns/op
PASS
ok     70.150s
  • BenchmarkCalCAllFruitPrice1-12: Fieldを集計したもの
  • BenchmarkCalCAllFruitPrice2-12: mapを集計したもの

1ベンチマークループで10.6倍程度の差が付きました。 動的に処理することや毎回の型検査が入ってくるのでパフォーマンスに大きな影響が出ていることが分かります。

オンラインのワークロードではない、速度を重要視しない内容や、非構造なデータを扱う時にReflectionは力を発揮するので、用法用量を守って利用しましょう。

参考

qiita.com