【Golang】Golang : `runtime` パッケージを使用したモニタリング
Golang のメモリ使用量やゴルーチン数など、実行時ランタイムに関係するステータスを観測するには、 runtime
パッケージを使用する。
Memory
var m runtime.MemStats runtime.ReadMemStats(&m) // ヒープ上に割り当てられたオブジェクト累積メモリ量 fmt.Printf("MAlloc : %v\n", humanize.Bytes(m.Mallocs)) // ヒープ上から開放されたオブジェクト数 fmt.Printf("Frees : %v\n", m.Frees) // ヒープ上に割り当てられたオブジェクトメモリ量 fmt.Printf("Alloc : %v\n", humanize.Bytes(m.Alloc)) fmt.Printf("HeapAlloc : %v\n", humanize.Bytes(m.HeapAlloc)) // ヒープ上に割り当てられたオブジェクトメモリ量。ただし開放されたオブジェクト分も含む fmt.Printf("TotalAlloc : %v\n", humanize.Bytes(m.TotalAlloc)) // OSから割り当てられたプロセスの総メモリ量 // ヒープ + スタック + その他 fmt.Printf("Sys : %v\n", humanize.Bytes(m.Sys)) // ポインタのルックアップ数 fmt.Printf("Lookups : %v\n", m.Lookups) // 到達可能、あるいはGCによって解放されていないヒープオブジェクトメモリ量 fmt.Printf("HeapAlloc : %v\n", humanize.Bytes(m.HeapAlloc)) // 未使用ヒープ領域メモリ量 fmt.Printf("HeapIdle : %v\n", humanize.Bytes(m.HeapIdle)) // 使用中ヒープ領域メモリ量 fmt.Printf("HeapInuse : %v\n", humanize.Bytes(m.HeapInuse)) // OSに返却される物理メモリ量 fmt.Printf("HeapReleased : %v\n", humanize.Bytes(m.HeapReleased)) // ヒープに割り当てられたオブジェクト量 fmt.Printf("HeapObjects : %v\n", m.HeapObjects) // 使用中スタック領域メモリ量 fmt.Printf("StackInuse : %v\n", humanize.Bytes(m.StackInuse)) // OSから割り当てられたスタック領域メモリ量 fmt.Printf("StackSys : %v\n", humanize.Bytes(m.StackSys)) // 割り当てられたmspan構造体バイト数 fmt.Printf("MSpanInuse : %v\n", humanize.Bytes(m.MSpanInuse)) // OSから取得したmspan構造体バイト数 fmt.Printf("MSpanSys : %v\n", humanize.Bytes(m.MSpanSys)) // 割り当てられたmcache構造体バイト数 fmt.Printf("MCacheInuse : %v\n", humanize.Bytes(m.MCacheInuse)) // OSから取得したmcache構造体バイト数 fmt.Printf("MCacheSys : %v\n", humanize.Bytes(m.MCacheSys))
MAlloc : 271 B Frees : 4 Alloc : 179 kB HeapAlloc : 179 kB TotalAlloc : 179 kB Sys : 70 MB Lookups : 0 HeapAlloc : 179 kB HeapIdle : 66 MB HeapInuse : 418 kB HeapReleased : 66 MB HeapObjects : 267 StackInuse : 229 kB StackSys : 229 kB MSpanInuse : 6.1 kB MSpanSys : 16 kB MCacheInuse : 21 kB MCacheSys : 33 kB
Goroutine 数
var m runtime.MemStats runtime.ReadMemStats(&m) // 3 goroutine go func() { time.Sleep(time.Second * 10) }() go func() { time.Sleep(time.Second * 10) }() go func() { time.Sleep(time.Second * 10) }() // 呼び出し時存在するゴルーチン数(main + 3) fmt.Printf("Goroutine : %v\n", runtime.NumGoroutine()) // n-GoroutineProfile : 4
ゴルーチン別メモリプロファイル
ゴルーチン毎のスタックトレースを uint32 配列で返す。
var m runtime.MemStats runtime.ReadMemStats(&m) sr := []runtime.StackRecord{ {}, {}, {}, {}, } n, ok := runtime.GoroutineProfile(sr) fmt.Printf("n-GoroutineProfile : %d\n", n) if ok { for i, p := range sr { fmt.Printf("GoroutineProfile-%d: %v\n", i, p.Stack0) } } // GoroutineProfile-0: [17609957 17607648 16957534 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] // GoroutineProfile-1: [17610480 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] // GoroutineProfile-2: [17610560 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] // GoroutineProfile-3: [17610640 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
参考文献
【Golang】Golang の map の非スレッドセーフ性と排他制御
Golang の map はスレッドセーフでない
Golang の map はスレッドセーフ、もといゴルーチンセーフではない。そのため、複数のゴルーチンからの同時アクセスによって整合性が保たれない状態になることがある。
例えば、以下のように map に対して複数のゴルーチンからの書き込みが発生すると、たまに非同期書き込み失敗のエラーが発生する。
package main func main() { kvs := NewKeyValueStore() for i := 0; i < 10; i++ { go func(kvs *KeyValueStore) { kvs.set("key", "value") kvs.get("key") }(kvs) } } type KeyValueStore struct { m map[string]string } func NewKeyValueStore() *KeyValueStore { return &KeyValueStore{m: make(map[string]string)} } func (s *KeyValueStore) set(k, v string) { s.m[k] = v } func (s *KeyValueStore) get(k string) (string, bool) { v, ok := s.m[k] return v, ok }
実行結果(たまに起こる)
fatal error: concurrent map writes
map がなぜデフォルトで非スレッドセーフになっているかというと、単純にパフォーマンス上不利であるためと思われる。この仕様のため、複数御ルーチンから map アクセスを実施するには明示的に排他制御を行う必要がある。1
map の排他制御
Go で排他制御を実現するには、sync.Mutex
を使用する。
package main import ( "fmt" "sync" ) func main() { kvs := NewConcurrentKeyValueStore() for i := 0; i < 10; i++ { go func(kvs *ConcurrentKeyValueStore) { kvs.set("key", "value") kvs.get("key") }(kvs) } } type ConcurrentKeyValueStore struct { m map[string]string mu sync.RWMutex } func NewConcurrentKeyValueStore() *ConcurrentKeyValueStore { return &ConcurrentKeyValueStore{m: make(map[string]string)} } func (s *ConcurrentKeyValueStore) set(k, v string) { s.mu.Lock() defer s.mu.Unlock() s.m[k] = v } func (s *ConcurrentKeyValueStore) get(k string) (string, bool) { s.mu.RLock() defer s.mu.RUnlock() v, ok := s.m[k] return v, ok }
上記のコードでは sync.RWMutex
を使用している。sync.RWMutex
は sync.Mutex
が提供する Lock()/Unlock()
のほか、 RLock()/RUnlock()
を提供する。
Lock()/Unlock()
は占有ロック。ロック時に対象への書き込み、読み込みを禁止する。RLock()/RUnlock()
は共有ロック。ロック時に対象への書き込みのみ禁止し、読み込みは禁止しない。
読み込み処理の場合、他ゴルーチンからの読み込み処理も考慮して RLock
を使用していくと良い。
Go の競合検出
Go ではコードの実行・ビルド時、あるいはパッケージインストール時に -race
オプションを指定することで、実行中の競合検出を行うことができる。
参考文献
【Golang】goimportsでプロジェクト配下のimport文すべてをパッケージ種類別に整形する
TL;DR
下記コマンドで、カレントディレクトリ配下のすべての .go
ファイルの import 文を整形してくれる。
$ find . -print | grep --regex '.*\.go' | xargs goimports -w -local "github.com/your/package"
goimports
の -w
オプションでファイルを直接書き換える。
また -local <string>
は指定した string
を prefix にもつパッケージの整形済み import 文の上部に、他の整形済み import 文が置かれる。
すなわち、string
が prefix の import 文のかたまりが最後にくるようになる。
よって、import 文整形順は 標準パッケージ
-> Publicパッケージ
-> 自パッケージ
の順となる。
あとは、find . -print | grep --regex '.*\.go'
で引っ掛けた .go
ファイル全てに上記 import 整形を適用する。
example
before
import ( "fmt" "github.com/rennnosuke/gih/domain/model/entity" "github.com/rennnosuke/gih/domain/service/git/issue" "github.com/urfave/cli/v2" "regexp" "strconv" "unicode/utf8" )
after
import ( // 標準パッケージ "fmt" "regexp" "strconv" "unicode/utf8" // Publicパッケージ "github.com/urfave/cli/v2" // 自パッケージ "github.com/rennnosuke/gih/domain/model/entity" "github.com/rennnosuke/gih/domain/service/git/issue" )
参考 : goimports
Go は標準でソースの import 文を整形するツール goimports
を提供している。
goimports
を Go ソースファイルに実行することで、ソースをコンパイルするのに必要なパッケージの import 文を自動挿入したり、インデントを追加したり、不要な import 文を削除してくれる。
before
package main func main() { s1 := "Hello" s2 := "goimports." s := strings.join(s1, s2, ",") fmt.Println(s) }
$ goimports -w main.go
after
package main import ( "fmt" "strings" ) func main() { s1 := "Hello" s2 := "goimports." s := strings.Join(s1, s2, ",") fmt.Println(s) }
参考文献
【Golang】Context
Golang における Context とは
大事なことは全部 Document に書いてある。おしまい。
context - The Go Programming Language
・・・と言ってしまうと元も子もないので、自分なりに整理してみる。
Go の Context を要約すると、
Web アプリケーションで横断的に使用するリクエストスコープ変数や処理を取り回す役割を持つもの
といえる。例えば、
が Context 内で管理される。
Go の net/http
ライブラリによって起動する Web サーバーは、ひとつのリクエストにひとつのゴルーチンを割り当てて処理を開始する。リクエストを受けて実行される処理の中で、ゴルーチンは更に増えていく。Context はそのように増えていくゴルーチン間で容易に値や処理を共有できる仕組みとして提供される。
Context=文脈といった意味だが、それこそアプリケーションの文脈では「あるスコープ内で横断的に共有されるモノ」といった意味合いで使用される。Go の Context も「1 リクエスト内で共有されるデータ・処理」といった意味合いで扱われている気がする。
Context の中身
Context
は以下のインタフェースによって定義されている。
type Context interface { Done() <-chan struct{} Err() error Deadline() (deadline time.Time, ok bool) Value(key interface{}) interface{} }
Done()
キャンセルかタイムアウトを検知できるチャネルを返す。このチャネルは Context
に対してキャンセルが通知されたか、タイムアウトが通知されたタイミングで閉じる。キャンセルは後述する WithCancel()
WithDeadline()
WithTimeout()
から返る関数を呼び出すと通知できる。タイムアウトは後述する WithDeadline()
WithTimeout()
関数で Context に設定できる。
Context を使う側は、この Done()
から返るチャネルを経由してリクエストの状態を知ることになる。
Err()
Context がキャンセルされた理由を含む error
値が返る。
キャンセルされない間、 Err()
は nil を返す。
Deadline()
Context がタイムアウトになるまでの時間と、Deadline が設定されているかどうかを返す。
Value(key interface{})
Context が保持する、 key
に紐づいた値を返す。
Value()
が返すべき値の管理はそれぞれの具象 Context 構造体にて定義される。
Context 単体だけではタイムアウトやキャンセルの通知はできない。Context は上記インタフェースの具象値だけでなく、後述する context
パッケージ内関数と組み合わせて使う。
Context のルール
保持する変数はリクエストスコープである
Value()
で習得できる値は、リクエストスコープ内で完結する必要がある。すなわち、複数リクエストから参照可能な値であってはならない。リクエストスコープはゴルーチン安全である リクエストスコープの値は、複数ゴルーチンから同時に参照されても安全、すなわちゴルーチンセーフ(?)である必要がある。
Context の仕組み
Context Tree
Context を使う場合、単一の Context だけをずっと使い回すわけではない。Context を使うアプリ内では、最初に生成した Context から次々と子 Context を派生させ、一つの Context Tree を築いていく。
context
パッケージには、 Context から子コンテキストを派生する関数が用意されている。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
これらの関数は引数の Context parent
から子 Context を派生させる。また同時に、リクエストがキャンセルされたことを通知するための関数も返す。このキャンセル関数を呼び出すと、生成された Context とその 子 Context にキャンセルが通知される。
キャンセルが Context に通知されると、Context が持つ Done()
チャネルが閉じ、 Err()
が error 値を返すようになる。返す error 値は関数生成元の関数による。
「子コンテキストの派生」「キャンセルの通知関数の生成」、そして子コンテキストへの性質の付与を同時に行う理由は、アプリケーションの境界によって渡す値や伝播するイベントを制御しやすくするため。例えば、データアクセス層へ渡す Context を WithTimeout()
で派生した子 Context にすることで、キャンセルイベントの伝播をデータアクセス層に閉じることができる。親 Context へはイベントは伝播しないため、上位層の Context について考えなくても良くなる。
Context を仕組みを助ける関数たち
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
引数に取る Context から新しい子 Context を生成・返戻し、加えてキャンセル関数を返す。
キャンセル関数は呼びだすと ctx
とその子 Context すべてにキャンセルが通知され、各 Context の Done()
チャネルが閉じ、 Err()
で error 値が得られるようになる。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
基本的には WithCancel
と同じ。異なる点は、指定した d
の時刻を経過したかどうかが Deadline()
から得られるようになるところ。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
基本的には WithDeadline
と同じ。異なる点は、Deadline を現在時刻からの経過時間で指定する点( WithDeadline(parent, time.Now().Add(timeout))
)。
func WithValue(parent Context, key, val interface{}) Context
生成する子 Context に値を設定する関数。子 Context は新たなリクエストスコープ値を持つことになる。
Context のつかいかた
実際に Context を使ってみた。下記コードの全体はGithub 上に置いてある。
Context の生成
下記コードでは、 Product
構造体配列を永続化層から取得して、標準出力へ書き出す処理を行っている。
func main() { r := repository.ProductRepositoryImpl{} s := service.ProductService{Repo: &r} ctx := context.Background() prods, err := s.GetProducts(ctx) if err != nil { panic(err) } fmt.Println(prods) }
type Product struct { Name string Price int }
Context の生成は context.Background()
で行う。生成した Context をアプリ内で呼び出す関数に渡していく。必要であれば、 context
パッケージ上の関数で子 Context に派生したり、タイムアウトを設けたり、新たなリクエストスコープ値を作ったりする。
type ProductRepositoryImpl struct{} // ProductService::GetProducts()が呼び出す、 // ProductRepository::GetProducts()関数の実装 func (r *ProductRepositoryImpl) GetProducts(ctx context.Context) ([]entity.Product, error) { c := make(chan []entity.Product, 1) childCtx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() go func(ctx context.Context) { c <- r.FindProducts(ctx) }(childCtx) select { case <-childCtx.Done(): return nil, errors.New("query canceled") case prods := <-c: return prods, nil } }
上記関数では、引数で受け取った Context からタイムアウトつき子 Context を派生して使用している。この Context から得られる Done()
チャネルを監視することで、キャンセルあるいはタイムアウトしたかどうかを検知することができる。
Context を運用する上で気をつけること
1. フレームワーク独自の Context との共用を避ける
フレームワークが独自に実装している Context でも標準パッケージ context
とほぼ同様のことができるが、そのような Context で Wrap したりすると、フレームワークに強く依存した実装になってしまう。
2. Value は不変とする
Context が保持するリクエストスコープ値は、複数ゴルーチンから参照されても安全でなければならない。
3.Context を他の構造体フィールドにしない
2.
より Context の保持する値はリクエストスコープで完結させたほうがよい。Context を別の構造体のフィールドとすると、その構造体の扱い次第ではアプリケーションスコープになりえるため、特に深刻な理由がなければ構造体フィールド化を避ける。
RootContext はひとつ
リクエストスコープ変数・シグナルを管理する機構が 2 つになるため、Context Tree が 2 つ以上になる状況は厳しい。
Context は nil で渡さない
関数で渡される Context は非 nil とする。どうしても渡す Context がない時のために、そのことを明示する Context を生成する context.Todo()
があるので、それを利用する。
参考文献
【Golang】APIMATIC で Postman Collection を OpenAPI Specification に変換する
PostmanはOpenAPIと互換性があり、OpenAPI Specification(以下OAS)に沿って書かれたAPIドキュメントはPostmanへimportできる。一方で、PostmanからOpenAPIへのexportはサポートされていない。これはPostmanへのFeature requestとしても挙がっており、そこそこ支持を得ている様子ではあるものの、3年前に提案されてからも未だに導入されていない。
Postmanを使ってAPIクライアントとのコミュニケーションを取りたいが、ドキュメントとして見た場合一部痒いところに手が届かない部分がある(BodyのDescription/Schemaを記述できないなど)。そのため、Postmanで記述したAPI仕様をOASに落として、管理は煩雑になるが仕様をより詳細に載せたverを作成できないかと考えた。
そこで、3rd partyから変換可能なものがないか探してみた。
Postman Collection -> OAS 変換可能な3rd partyのツール/サービス
stoplightio/api-spec-converter
Spotlight.ioでも使用されているライブラリ。Postman Collection v1のみ対応。
JSライブラリのため、変換用のコードをJSで記述する必要がある。ただ実際に変換を検証したところ、v1形式のPostman Collectionを変換しようとしたが、うまく変換できなかった。。。
また、3年前ほど前から保守がストップしている。そのため今回は使用を見送り。
APIMATIC
APIMATIC社が提供するSaaS。Postman Collection v2の変換に対応している。
アカウント登録が必要だが、結果から言うとドキュメント変換がきれいに実行できた。今回はこのツールを使用してみる。
APIMATICで Postman Collection を OASに変換する
はじめに、Postman上に定義されたAPI仕様をexportする。
exoprtしたいCollectionの三点リーダをクリックし、出現したリストの Export
をクリックする。
するとexportするファイルのバージョンを選択できるので、ここではv2.1を選択。
これで、 Postman Collection
と呼ばれる json
ファイル形式のAPIドキュメントが出力される。
このファイルはPostman間の定義や設定共有に使用されるものだが、今回はその中のAPI定義部分を抽出し、OASとして変換する。
次に、 APIMANICのWebページを開き、 Sign Up for free
をクリック。
新規アカウント登録画面に遷移するので、必要な情報を入力し、 Sign Up
をクリック。
しばらくすると登録したメールアドレスにconfirmメールが来るので、メール内のconfirmボタンをクリックし、 Click here
をクリックする。
APIMATICダッシュボード画面に遷移する。 Transform API
をクリック。
先程exportした Postman Collection
jsonファイルを選択し、 好きな形式を選択してexportできる。今回はOAS3.0を選択。Convertボタンをクリック。
最後にうまく変換できたかどうかが表示されるので、確認した上でProceedボタンを押して変換後ファイルをダウンロード。
完了。
以下のようなOAS3.0形式ファイルが出力される。
openapi: 3.0.0 info: title: test contact: {} version: '1.0' servers: - url: http://hoge.api.com variables: {} paths: /test: get: tags: - Misc summary: http://hoge.api.com/test description: http://hoge.api.com/test operationId: http://hoge.api.com/test parameters: [] responses: 200: description: '' headers: {} deprecated: false tags: - name: Misc description: ''
これで、 OpenAPI形式への変換が完了した。
これでOASに合わせて記述を追加したり、OAS対応のサービスでおしゃれにレンダリングしたりできる。
参考文献
【Golang】Go のSSA表現形式のダンプとその解析
SSA 形式とは
静的単一代入(static simple-assignment)形式と呼ばれ、コンパイラによって生成される中間表現のこと。 SSA は、各変数への代入が一度のみ行われる式によって構成される。
例えば、以下のような式を含むコードを見ると、一行目のコードが不要であることが明らかにわかる。ただし、プログラム上で一行目を判断するためには、 Reaching-Defenitionのような分析手法とそれを実現するアルゴリズムを実行しなければならない。
y := 1 y := 2 x := y
(上記擬似コードでは :=
で変数の宣言・代入を表現している。重複した宣言は許可されている)
SSA 表現形式 では、変数への代入がただ一度になるようにコードが変換される。
上記のコードは、SSA 形式の表現では以下のように表現される。
y_1 := 1 y_2 := 2 x_1 := y_2
SSA では、重複する変数には各変数を別物として扱うため添字が付与される。
上記の SSA 形式表現では y_1
が参照されないことは明らかであり、プログラム上からもデッドコードとして容易に判断できる。
ちなみに、分岐内で同じ変数への代入が発生しても、SSA 形式では別の変数への代入として表現される。分岐終了後に元の変数への参照が発生した場合は、SSA 表現内では分岐によって参照する変数を切り替える phi
関数(実際には条件によって切り替わるマーカー)を参照する。
x := 1 if x > 2 { x := 2 } else { x := x + 1 } y := x * 2 x := y
x_1 := 1 if x_1 > 2 { x_2 := 2 } else { x_3 := x_1 + 1 } y_1 := phi(x_2, x_3) * 2 x_4 := y_1
この SSA 形式表現は、上記のデッドコードの発見・削除のようなコンパイラ最適化処理のために利用できる。
Go においても、この中間表現によるコンパイル最適化が行われている。
Go コードから SSA 形式表現を出力する
Go による SSA 形式出力は Go1.7 よりサポートされた。
GOSSAFUNC
環境変数を変更してgo build
go tool compile
オプション指定
のどちらかを実行すると、SSA 形式表現を記述したファイルをダンプすることができる。
GOSSAFUNC
環境変数を変更して go build
する
GOSSAFUNC
環境変数にターゲットとなる関数を指定して .go
ファイルをビルドすると、 .html
形式の SSA 形式表現ファイルが得られる。
$ env GOSSAFUNC=[関数名] go build main.go
この html
ファイルには標準の SSA 形式表現のほか、最適化後の SSA 形式表現なども描画されている。
go tool compile
オプションを指定する
go tool compile
のオプション 'ssa'
を指定することでも SSA 形式を出力できる。
$ go tool compile -d 'ssa/build/dump=main' main.go
ssa/help
を指定することで、オプションの使用方法を確認できる。
$ go tool compile -d 'ssa/help' compile: PhaseOptions usage: go tool compile -d=ssa/<phase>/<flag>[=<value>|<function_name>] where: - <phase> is one of: check, all, build, intrinsics, number_lines, early_phielim early_copyelim, early_deadcode, short_circuit, decompose_args decompose_user, opt, zero_arg_cse, opt_deadcode, generic_cse, phiopt nilcheckelim, prove, fuse_plain, decompose_builtin, softfloat, late_opt dead_auto_elim, generic_deadcode, check_bce, branchelim, fuse, dse writebarrier, insert_resched_checks, lower, lowered_cse elim_unread_autos, lowered_deadcode, checkLower, late_phielim late_copyelim, tighten, late_deadcode, critical, phi_tighten likelyadjust, layout, schedule, late_nilcheck, flagalloc, regalloc loop_rotate, stackframe, trim - <flag> is one of: on, off, debug, mem, time, test, stats, dump - <value> defaults to 1 - <function_name> is required for the "dump" flag, and specifies the name of function to dump after <phase> Phase "all" supports flags "time", "mem", and "dump". Phase "intrinsics" supports flags "on", "off", and "debug". If the "dump" flag is specified, the output is written on a file named <phase>__<function_name>_<seq>.dump; otherwise it is directed to stdout. Examples: -d=ssa/check/on enables checking after each phase -d=ssa/all/time enables time reporting for all phases -d=ssa/prove/debug=2 sets debugging level to 2 in the prove pass Multiple flags can be passed at once, by separating them with commas. For example: -d=ssa/check/on,ssa/all/time
例えば、 'ssa/build/dump=[関数名]'
を指定して、SSA 形式表現のダンプファイル( .dump
)を得る。
$ go tool compile -d 'ssa/build/dump=main' main.go
$ ls main.go main.o main_01__build.dump
'ssa/opt/dump=[関数名]'
を指定すると、最適化後の SSA 形式表現のダンプファイル() .dump
)を得られる。
Go の SSA を解析する
実際に、SSA 形式表現の Go コードをダンプしてその中身を見てみる。
hoge.go
package hoge func hoge() int { x := 1 return x }
用意したのは素朴な関数一つを宣言した .go
ファイル。
関数 hoge
内では変数 x
を宣言、 整数 1
を代入し関数の返戻値としている。
この関数のダンプを取ると以下のようになる。
hoge func() int b1: // ブロックラベル v1 = InitMem <mem> // ヒープメモリ領域の初期化。先頭アドレスがv1に格納される。 v2 = SP <uintptr> // スタックポインタ(スタックの先頭アドレス) v3 = SB <uintptr> DEAD // スタックベース(スタックの底アドレス。デッドコード) v4 = LocalAddr <*int> {~r0} v2 v1 // int型のローカルアドレス : v2上にマウントされる。{~r0}は結果を格納する変数 v5 = Const64 <int> [0] DEAD // int型定数 (x := 1のうち 宣言 var x; の部分。デッドコード) v6 = Const64 <int> [1] (x[int]) // int型定数 (x := 1のうち 代入 x = 1; の部分。) v7 = VarDef <mem> {~r0} v1 // 新規変数{~r0}を定義。参照先はv1 v8 = Store <mem> {int} v4 v6 v7 // v7上のメモリでv6 を v4に格納する。該当するアドレスがv8に格納される。 Ret v8
上記 SSA の変数のうち、 v3
v5
は使われていないためデッドコード扱いとなっている。
デッドコードを削除し、最適化された SSA 形式表現は以下のようになる。
hoge func() int b1: v1 = InitMem <mem> v2 = SP <uintptr> v4 = LocalAddr <*int> {~r0} v2 v1 v6 = Const64 <int> [2] (x[int]) v7 = VarDef <mem> {~r0} v1 v8 = Store <mem> {int} v4 v6 v7 Ret v9
表現形式こそ異なれど、処理の流れは Go バイナリと同じなのでバイナリ実行時も同様の最適化が行われている。
このように、最適化前後の SSA 形式表現を比較することでどの処理が最適化されているのか把握できる。
試しに、 hoge.go
を以下のように改変してみる。
package hoge func hoge() int { x := 1 x = 2 return x }
明らかに、 x := 1
における代入部分が無駄なことがわかる。
SSA に起こすと以下のようになる。
hoge func() int b1: v1 = InitMem <mem> v2 = SP <uintptr> v3 = SB <uintptr> DEAD // デッドコード v4 = LocalAddr <*int> {~r0} v2 v1 v5 = Const64 <int> [0] DEAD // x = 0 (x := 1 の宣言、デッドコード) v6 = Const64 <int> [1] (x[int]) DEAD // x = 1 (x := 1 の代入、デッドコード) v7 = Const64 <int> [2] (x[int]) // x = 2 v8 = VarDef <mem> {~r0} v1 v9 = Store <mem> {int} v4 v7 v8 Ret v9
代入 x = 2
が一つ増えていることがわかる。
また x = 2
によって x = 1
の代入がデッドコードになっている。
これを最適化すると、
hoge func() int b1: v1 = InitMem <mem> v2 = SP <uintptr> v4 = LocalAddr <*int> {~r0} v2 v1 v7 = Const64 <int> [2] (x[int]) v8 = VarDef <mem> {~r0} v1 v9 = Store <mem> {int} v4 v7 v8 Ret v9
のように変数ラベルこそ異なるものの同じ最適化結果となる。
参考文献
【Golang】Go 1.14 の重複するメソッドを持つインターフェース埋め込みの許可
proposal/6977-overlapping-interfaces.md at master · golang/proposal
Go 1.14 より、重複するメソッドを持ったインタフェースの複数埋め込みが可能になった。
インタフェースの埋め込みは、あるインタフェース A を別のインタフェース B 内で宣言することで、A で宣言したメソッドを B でも宣言したことにできる機能。
// on Package io : // type ReadCloser interface { // Reader // Closer // } import io type CustomReadCloser interface { io.ReadCloser isClosed() bool }
Go1.13 までは、言語仕様としてインタフェースのメソッドには一意性制約があるため、B が A の宣言するメソッドを既に宣言していたり、埋め込んだインタフェース同士が同じメソッドを持つとコンパイルエラーとなっていた。
Go1.14 では「埋め込んだインタフェース同士が同じメソッドを持つ」場合に関してはコンパイルが通る様になる。
例えば、下記のインタフェース埋め込みは Go 1.13 以前は不可能だったが、1.14 より可能になる。
// on Package io : // type ReadCloser interface { // Reader // Closer // } // type WriteCloser interface { // Writer // Closer // } import io type ReadWriteCloser interface { io.ReadCloser io.WriteCloser }
io.ReadCloser
と io.WriteCloser
は共に Close()
メソッドを宣言しているが、Go1.14 ではコンパイルが通る。
ただし、メソッドの名称が同一だがシグネチャが異なる場合、埋め込みは許可されない。
type E1 interface { m(x int) bool } type E2 interface { m(x float32) bool } type I interface { E1 E2 }
議論として、埋め込まれた側のインタフェースが埋め込みインタフェースと同一のメソッドを持つ場合どうするか問題が議論に上っていたが、上記の通りGo の言語仕様のインタフェース一意性の項目に
Explicitly declared methods in an interface must remain unique, as before.
と書かれているため、多分許可されないままだと思われる。