Shogo's Blog

Mar 17, 2017 - 2 minute read - go golang

Go言語のchanはいったいいくつ付けられるのか試してみた

pecoに入った修正をみて、果たしてchanはいくつまで付けられるのか気になったので、 雑に試してみました。 先に断っておきますが、全く有用ではないですよ。

背景

pecoに入った修正はこちら(一部抜粋)。

diff --git a/interface.go b/interface.go
index 3d4472f..fff446c 100644
--- a/interface.go
+++ b/interface.go
@@ -162,8 +162,8 @@ type Screen interface {
 // Termbox just hands out the processing to the termbox library
 type Termbox struct {
 	mutex     sync.Mutex
-	resumeCh  chan (struct{})
-	suspendCh chan (struct{})
+	resumeCh  chan chan struct{}
+	suspendCh chan struct{}
 }
 
 // View handles the drawing/updating the screen
diff --git a/screen.go b/screen.go
index edbce87..f6dd71e 100644
--- a/screen.go
+++ b/screen.go
@@ -21,7 +21,7 @@ func (t *Termbox) Init() error {
 func NewTermbox() *Termbox {
 	return &Termbox{
 		suspendCh: make(chan struct{}),
-		resumeCh:  make(chan struct{}),
+		resumeCh:  make(chan chan struct{}),
 	}
 }

channelを使ってchannelをやり取りすることができるので、 chan struct{}をやり取りするchan chan struct{}という型が使えます。 同じ要領で、channelをやり取りするchannelをやり取りするchannelをやり取り…するchannelが 無限に作れるはずです(少なくとも構文上は)。 ということで、実際にやってみました。

実験

雑なPerlスクリプトを準備して、大量のchanを付けたGoのコードを自動生成します。

print <<EOF;
package main

import (
    "fmt"
)

type Foo @{['chan ' x 4096]} struct{}

func main() {
    fmt.Printf("Hello, %#v\\n", make(Foo))
}
EOF

chanの個数を変えて何度かビルドを繰り返します。

time go build -o main main.go

結果

chanの個数とビルドにかかった時間をまとめてみました。

chanの個数ビルド時間
10.236s
20.240s
40.226s
80.234s
160.240s
320.250s
640.281s
1280.258s
2560.360s
5120.775s
10243.228s
204818.605s
40961m53.614s
819213m46.018s(ビルド失敗したので参考記録)

8192個付けたら以下のようなエラーを吐いてビルドが失敗してしまったので、 8192個の時の記録は参考記録です。

# command-line-arguments
too much data in section SDWARFINFO (over 2000000000 bytes)

何かビルドの設定をいじればもっと行けるかもしれませんが、 デフォルトの設定では4096から8192の間に限界があるようです。 4096個chanを付けたときのソースコードは20KB程度なのにバイナリサイズは524MBまで膨らんでいました。

256個当たりからビルド時間に影響が出ているので、 ビルド時間を考える256個以下に抑えるのがよさそうです。 それ以上だと $O(n^{2.6})$ 程度のオーダーでビルド時間が延びます。 とはいえ、256個もchanを付いたコードを人間が読めるとは思えないので、 2個が限度でしょうね・・・。 3個以上必要になるケースは余りないと思います。

型定義を再帰的にして無限chanを実現する

そもそも、chanを大量に並べなくとも、 型定義を再帰的に行えば無限のchanを付けたときと同等のことができます。 例えば以下のコードで"Goroutine 1"と"Goroutine 2"を交互に表示することが可能です。

package main

import (
	"fmt"
)

type Foo chan Foo

func main() {
	ch := make(Foo)
	go func() {
		ch := ch
		for {
			done := <-ch
			fmt.Println("Goroutine 2")
			done <- ch
		}
	}()
	
	for i := 0; i < 100; i++ {
		fmt.Println("Goroutine 1")
		done := make(Foo)
		ch <- done
		ch = <-done
	}
	fmt.Println("Hello, playground")
}

channelでのやり取りが複雑になるので実用性があるかは不明ですが・・・。 例えば先程の例だと、普通にループを書いたほうが圧倒的にシンプルです。

package main

import (
	"fmt"
)

func main() {
	for i := 0; i < 100; i++ {
		fmt.Println("Goroutine 1")
		fmt.Println("Goroutine 2")
	}
	fmt.Println("Hello, playground")
}

無限chanが必要になる多くのケースは、このような書き換えができるような気がします。 (そもそも必要になったことがない)

まとめ

  • chanの個数の上限は4096から8192の間のどこか
  • 256個あたりからビルド時間に影響が出始める
    • プログラムを読む人の精神力に多大な影響を与えるので、実際は2個までに留めるべきだと思う
  • 再帰的に型を定義することで、無限にchanを付けた時と同等のことが可能

chanを大量に付けたいケースには今までに僕自身は遭遇したことがないです。 有用な例を見つけた人は教えてください。