GoのHTTP実装を読んだ知見をまとめる~FIFOを5通りで実装する~

# はじめに
引き続き、GoのHTTP実装と絡めて書く。今日はFIFO実装について。
経緯は[こちら](https://qiita.com/behiron/items/78d023be96058224e583#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB)

[Russ CoxによるHTTPクライアントのFIFO実装](https://github.com/golang/go/blob/682a1d2176b02337460aeede0ff9e49429525195/src/net/http/transport.go#L1242-L1306)が工夫されていて非常に興味深かった。
コメントを見ると、[Okasaki's purely functional queue](https://www.amazon.co.jp/-/en/Chris-Okasaki/dp/0521631246)を元に、少し工夫したものらしい。

今回はそれらに触れた上でGoでFIFOを5通りの方法で実装していきたいと思う。

実装およびテストコードは[GitHub](https://github.com/KeiichiHirobe/go-http-learning/tree/main/fifo)にあげてあります。

# FIFO実装
実装方法を紹介していく。
ここでは、簡単なため、自分が実装した型名をもとに紹介していきます。

TwoListSlice実装はほぼコピペなので実装は正しいはずです。その他は何も参考にせずに実装したものなのでバグがあるかもしれません。悪しからず。

## SList
とてもシンプルな実装で、ただのsingly linked listです。
各要素は値と次要素へのリンクを保持し、リスト構造としてはheadとtailを保持します。

もっともオーソドックスな実装なため説明は省きますが、一点補足すると、`PopFront`時に返却する要素のリンクをnilで上書きしています。これはGCにより回収されるようにするためで、[Goの標準ライブラリのdoubly linked list実装](https://github.com/golang/go/blob/682a1d2176b02337460aeede0ff9e49429525195/src/container/list/list.go#L111)を参考にしたものです。

## PList
これは単純にSListに対して`PushBack`と`PopFront`時に全要素をコピーして作り直します。
[SListのテストコード](https://github.com/KeiichiHirobe/go-http-learning/blob/1b07aa8247d3e1bfe1580fa943efc96a6bc6964a/fifo/singly_list_test.go#L30-L53)に書いたようにSListはpersistentでないため、キューAをキューBに代入した後キューAの操作がキューBに影響を与えてしまいます。

これを単純に回避するために全要素を操作時に毎回コピーしています。

この実装は明らかに重く、TwoListの方が良い実装です。

## TwoList
これは[Okasaki's purely functional queue](https://www.amazon.co.jp/-/en/Chris-Okasaki/dp/0521631246)の中で紹介されているpersistentなFIFO実装です。

本に興味はありますが私は読んでません。[20分でわかるPurely Functional Data Structures](http://www.kmonos.net/pub/Presen/PFDS.pdf)がとてもわかりやすく、TwoListの詳細な説明はそちらに譲ります。

TwoListだけであれば5分くらいで理解できると思います。銀行家キューが少し難しく、それ以降は自分はよく理解していません。

簡単にエッセンスを説明すると、キューを内部的に二つもちます。`head`から始まるキュー、`tail`から始まるキューを持ち、それぞれsingly linked list(それぞれリストの先頭要素のみ保持すればよいのがSListとは異なる)とします。

`PushBack`時には`tail`から始まるキューの先頭（**末尾ではない**）に挿入します。`PopFront`時には`head`から始まるキューの先頭から取得しますが、空の場合は`tail`から始まるキューをreverseして`head`から始まるキューとしてswitchingしてから先頭から取得します。

これだけでpersistentになるのすごくないですか？？
騙された気分になりましたが、「persistentにするには、更新操作において、元構造から辿れるデータには更新は発生させないようにする」のがポイントと自分は理解しました。

reverseするのは一見計算量として不利になりそうですが、そうでもありません。また、さらなる改良をあり、そのあたりは先ほどの資料をみると良いと思います。

## NaiveListSlice
リストではなく、goのSliceを利用した実装です。[テスト](https://github.com/KeiichiHirobe/go-http-learning/blob/4633b90476775bc5cf46f8b28dfb2e194691a94a/fifo/naive_list_slice_test.go#L47)からわかるようにpersistentではありません。goのsliceは内部的に配列を参照し、データを共有するためです。
十分にシンプルなのでそのままコードを載せて説明は省略します。

```golang

package fifo

type NaiveListSlice []interface{}

func (l *NaiveListSlice) Len() int {
	return len(*l)
}

func (l *NaiveListSlice) PushBack(v interface{}) {
	*l = append(*l, v)
}

func (l *NaiveListSlice) PopFront() interface{} {
	list := *l
	if len(list) == 0 {
		return nil
	}
	v := list[0]
	*l = list[1:]
	return v
}
```

## TwoListSlice
[Russ CoxによるHTTPクライアントのFIFO実装](https://github.com/golang/go/blob/682a1d2176b02337460aeede0ff9e49429525195/src/net/http/transport.go#L1242-L1306)で利用されている実装です。
^のコメントをそのまま載せちゃいます。

```golang

// A wantConnQueue is a queue of wantConns.
type wantConnQueue struct {
	// This is a queue, not a deque.
	// It is split into two stages - head[headPos:] and tail.
	// popFront is trivial (headPos++) on the first stage, and
	// pushBack is trivial (append) on the second stage.
	// If the first stage is empty, popFront can swap the
	// first and second stages to remedy the situation.
	//
	// This two-stage split is analogous to the use of two lists
	// in Okasaki's purely functional queue but without the
	// overhead of reversing the list when swapping stages.
	head    []*wantConn
	headPos int
	tail    []*wantConn
}
```

コメントの通りですが、TwoListと同様に二つのキューを利用しており、Sliceで実現します。ポイントは

* `headPos`を利用して、sliceを進めさせないこと(NaiveListSliceでいうところの`*l = list[1:]`はしない)
* switchingは発生するが、その際にreverseが発生しないこと

だと思いました。なお、[テスト](https://github.com/KeiichiHirobe/go-http-learning/blob/1b07aa8247d3e1bfe1580fa943efc96a6bc6964a/fifo/two_list_slice_test.go#L27)からわかるように、この構造自体は**persistentではない**ので注意が必要です。

この構造はNaiveListSliceのような単純なFIFO実装と比べるとメモリを使い回すことができ、`PushBack`/`PopFront`操作が一定回数以上続くような場合に良いと思われます。

とても単純な`PushBack`と`PopFront`が交互に一回ずつ続くケースを考えてみましょう。

NaiveListSliceの実装では、`PushBack`時の`append`において毎回1要素分のメモリ確保が発生します。なお、`append`実装に詳しくありませんが、自分の環境では1 -> 2 -> 4-> 8とメモリを確保していたのでそれ前提になります。

TwoListSlice実装では、最初の２回の`PushBack`のみ1要素分のメモリ確保を行い、その後は確保済みのメモリを使いまわします。

逆にいうと、使い回すからこそpersistentになりえないです。


# HTTP実装
TwoListSliceですが、HTTPクライアントの`idleConnWait`と`connsPerHostWait`に利用されています。

両方とも接続先のホスト(厳密には[connectMethodKey](https://github.com/golang/go/blob/682a1d2176b02337460aeede0ff9e49429525195/src/net/http/transport.go#L1846-L1852))ごとに保持されます。それぞれ簡単に説明します。

`idleConnWait`はidleとなるconnectionを待つキューです。idleなコネクションプールではなく、待つキューであることに注意してください。

idleなコネクションプールは全体の制限である`MaxIdleConns(デフォルト100)`とホストごとの制限である`MaxIdleConnsPerHost(デフォルト2)`により制限があります。

クライアントはリクエストを行う際、idleなコネクションが利用できないとわかると、`idleConnWait`に「コネクションが欲しいという情報」を`PushBack`した後、接続先に対して`dial`します。`dial`の結果を受け取るより先に、`idleConnWait`での待ち順番が回り、idleなコネクションが利用できるようになった場合にはそれを利用し、不要となった`dial`結果のコネクションはidleなコネクションとして他のリクエストに再利用されます。そうでない場合は普通に`dial`の結果のコネクションをリクエストに利用します。

`connsPerHostWait`はもっと簡単です。`MaxConnsPerHost(デフォルト無効)`によりホストごとのコネクション数が制限されていて、その制限を超過する場合、接続を試みる前に`connsPerHostWait`に`PushBack`されます。待ち順番が回り、接続を試ることが許可された場合、`PopFront`されます。

では、なぜこれら二つのFIFOキュー実装にTwoListSlice構造を利用したのでしょうか。

それは、リクエストが一時的に多くなり、`PushBack`と`PopFront`が連続的に行われ、キューがなかなか空にならないことを想定し、メモリを使いまわせるためだと思います。

実際、[この実装がされた当初のレビュー](https://go-review.googlesource.com/c/go/+/184262/2..4/src/net/http/transport.go#b8430)の際には以下のようにRuss Coxがコメントしてました。

> I wanted to do something that would work well for an extreme case where there are many requests queued up on a small per-host limit, so I was careful in how I wrote the wantConnQueue,

# おわりに
本当はキリもいいですし、銀行家キューも実装して「FIFOを6通りで実装する」としたかったのですが、銀行家キューは「遅延評価」、「メモ化」などの実装を関数型でないGoでおこなうのがやや大変そうだったので今回はあきらめました。

そのうち、銀行家キューも実装しようとおもってます。