🤖

インターフェースからのメソッド呼び出しはちょっと遅いので代わりにクロージャにすると改善する

2024/02/23に公開

背景

Go言語のinterfaceでのメソッド呼び出しは動的ディスパッチになるため少しオーバーヘッドがあるという話を聞いた。ちょっとGo言語を書いていたところクロージャにするとinterfaceの代わりにでき、静的ディスパッチになるのではないかと考えた。
ということで試してみた。

結論

クロージャで書くとオーバーヘッドが少ない

Go言語で実装の詳細隠蔽する手段としてinterfaceがあるが、interfaceが持つメソッドが一つであ
れば、クロージャで書き換えられる。

考察

  • ポイントを引数に関数を呼び出しをするとコストになるのではないか
      - 値を書き換えるためにポインタで呼び出したところ少し遅くなったため
  • メソッドを直接渡すにはコストがあって即時関数でラップするほうがコストが削減できるのではないか
    • 関数を引数にとる関数にメソッドを直接よりラップしたほうが早かったため

検証内容

以下のようなコードを書いた。

package main

import (
	"testing"
)

type AddInterface interface {
	Add(int) int
}

// 実体でメソッドを実装
type AddEntity struct {
	v int
}

// Pointerでメソッドを実装
type AddPointer struct {
	v int
}

// クロージャを作って呼び出し用の型
type AddFunc func(int) int

// 値レシーバで実装する用の構造体
func (a AddEntity)Add(v int) int {
	a.v = a.v + v
	return a.v
}

// ポインタレシーバで実装する用の構造体
func (a *AddPointer)Add(v int) int {
	a.v = a.v + v
	return a.v
}

func callInterface(adder AddInterface) {
	_ = adder.Add(1)
}

func callFunc(adder AddFunc) {
	_ = adder(1)
}


func callPointerRef(adder *AddPointer) {
	_ = adder.Add(1)
}

func callPointerDirect(adder AddPointer) {
	_ = adder.Add(1)
}


func callEntityDirect(adder AddEntity) {
	_ = adder.Add(1)
}

func callEntityRef(adder *AddEntity) {
	_ = adder.Add(1)
}


// ポインタレシーバで実装したinterfaceを呼び出し(遅い)
func BenchmarkInterfacePointer(b *testing.B) {
	adder:= AddPointer{v: 1}
	for i := 0; i < b.N; i++ {
		callInterface(&adder)
	}
}

// 値レシーバで実装したinterfaceを呼び出し(遅い)
func BenchmarkInterfaceEnity(b *testing.B) {
	adder := AddEntity{v: 1}
	for i := 0; i < b.N; i++ {
		callInterface(adder)
	}
}

// 値レシーバで実装したメソッドを直接呼び出し(速い)
func BenchmarkEntityDirect(b *testing.B) {
	adder := AddEntity{v: 1}
	for i := 0; i < b.N; i++ {
		callEntityDirect(adder)
	}
}

// 値レシーバで実装したメソッドをポインタ経由で呼び出し(速い)
func BenchmarkEntityRef(b *testing.B) {
	adder := &AddEntity{v: 1}
	for i := 0; i < b.N; i++ {
		callEntityRef(adder)
	}
}

// ポインタレシーバで実装したメソッドを直接呼び出し(速い)
func BenchmarkPointerDirect(b *testing.B) {
	adder := AddPointer{v: 1}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		callPointerDirect(adder)
	}
}


// ポインタレシーバで実装したメソッドをポインタ経由で呼び出し(ちょっと遅い)
func BenchmarkPointerRef(b *testing.B) {
	adder := &AddPointer{v: 1}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		callPointerRef(adder)
	}
}

// 値の書き換えがあるクロージャの呼び出し(ちょっと遅い)
func BenchmarkMutableClosure(b *testing.B) {
	v := 0
	adder := func (n int) int {
		v = v + n
		return v 
	}
	for i := 0; i < b.N; i++ {
		callFunc(adder)
	}

}

// 値の書き換えのないクロージャの呼び出し(速い)
func BenchmarkImmutableClosure(b *testing.B) {
	v := 0
	adder := func (n int) int {
		_ = v+n
		return v
	}
	for i := 0; i < b.N; i++ {
		callFunc(adder)
	}

}

// 値レシーバ実装したメソッドを関数として使う(普通)
func BenchmarkMethodEntity(b *testing.B) {
	adder := AddEntity{v: 1}
	for i := 0; i < b.N; i++ {
		callFunc(adder.Add)
	}
}

// ポインタレシーバ実装したメソッドを関数として使う(遅い)
func BenchmarkMethodPointer(b *testing.B) {
	adder := AddPointer{v: 1}
	for i := 0; i < b.N; i++ {
		callFunc(adder.Add)
	}
}

// ポインタレシーバ実装したメソッドをポインタ経由で関数として使う(遅い)
func BenchmarkMethodPointerRef(b *testing.B) {
	adder := &AddPointer{v: 1}
	for i := 0; i < b.N; i++ {
		callFunc((adder).Add)
	}
}

// 値レシーバ実装したメソッドを即時関数でラップして使う(速い)
func BenchmarkFuncLiteralEntity(b *testing.B) {
	adder := AddEntity{v: 1}
	for i := 0; i < b.N; i++ {
		callFunc(func (n int) int { return adder.Add(n)})
	}
}

// 値レシーバ実装したメソッドを即時関数でラップして使う(ちょっと遅い)
func BenchmarkFuncLiteralPoirnter(b *testing.B) {
	adder := &AddPointer{v: 1}
	for i := 0; i < b.N; i++ {
		callFunc(func (n int) int { return adder.Add(n)})
	}
}

実行結果

go test -bench . -benchmem
goos: darwin
goarch: arm64
pkg: call
BenchmarkInterfacePointer-12       	573817428	         1.959 ns/op	       0 B/op	       0 allocs/op
BenchmarkInterfaceEnity-12         	615608892	         1.968 ns/op	       0 B/op	       0 allocs/op
BenchmarkEntityDirect-12           	1000000000	         0.2909 ns/op	       0 B/op	       0 allocs/op
BenchmarkEntityRef-12              	1000000000	         0.2887 ns/op	       0 B/op	       0 allocs/op
BenchmarkPointerDirect-12          	1000000000	         0.2930 ns/op	       0 B/op	       0 allocs/op
BenchmarkPointerRef-12             	1000000000	         1.081 ns/op	       0 B/op	       0 allocs/op
BenchmarkMutableClosure-12         	1000000000	         1.078 ns/op	       0 B/op	       0 allocs/op
BenchmarkImmutableClosure-12       	1000000000	         0.2943 ns/op	       0 B/op	       0 allocs/op
BenchmarkMethodEntity-12           	1000000000	         0.8698 ns/op	       0 B/op	       0 allocs/op
BenchmarkMethodPointer-12          	633338822	         1.887 ns/op	       0 B/op	       0 allocs/op
BenchmarkMethodPointerRef-12       	634365564	         1.894 ns/op	       0 B/op	       0 allocs/op
BenchmarkFuncLiteralEntity-12      	1000000000	         0.2890 ns/op	       0 B/op	       0 allocs/op
BenchmarkFuncLiteralPoirnter-12    	1000000000	         1.090 ns/op	       0 B/op	       0 allocs/op

Discussion