1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Golang による順列列挙のパフォーマンス研究 2. スタックを用いたやり方

Last updated at Posted at 2020-12-12

関連記事

本記事は Geek-Space Advent Calendar 2020 の 5 日目です。(間に合ってない)

はじめに

前回、DFS には再帰を用いるやり方とスタックを用いるやり方があるということで、再帰を用いるやり方の方を探っていきました。
今回はスタックを用いるやり方について同様に探っていきます。
再帰呼び出しは言語によって重かったりとかもあると思うので、再帰を避けることでもっとパフォーマンスの良い実装が見つかるといいなぁという感じです。
前回得られた「使用可能な数字をチェックリスト式で管理した方が良さそう」「1 つの配列を全順列分使い回した方が良い」といった結論は、今回は自明になったものとして最初から導入していきます。

コードは前回同様、全てこちらのリポジトリに上げてあります。
https://github.com/ikngtty/benchmark-go-combinatorics

実装

下準備

計測環境については前回と同様なので省略。

コールスタックを再現する

「再帰呼び出し」と「スタック」の共通点を考えてみて、「そういえば再帰呼び出しはコール"スタック"を積み上げてるな…」と思い浮かびました。そこから「コールスタックもどきを自分で実装すれば再帰しなくてもいいのでは?」と思い至ったので、試してみることにします。

コールスタックについて詳しく知っているわけではないですが、積む必要がありそうな情報はおそらく、ある一つの実行中の関数呼び出しについて

  • 各引数や変数の中身
  • どこまでコードを実行したか

あたりじゃないでしょうか。

ちょっと再帰の場合のコードを見てみましょう。

combinatorics/perm.go
func PermutationsRecursive6(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	pattern := make([]int, k)

	var body func(pos int)
	body = func(pos int) {
		if pos == k {
			f(pattern)
			return
		}

		for num := range checklist {
			if checklist[num] {
				continue
			}

			pattern[pos] = num
			checklist[num] = true
			body(pos + 1)
			checklist[num] = false
		}
	}
	body(0)
}

この body 関数を繰り返し呼んだ時、コールスタックには

  • 各引数や変数の中身
    • pos の中身
    • num の中身
  • どこまでコードを実行したか
    • checklist[num] = false の直前まで実行した

といった情報がどんどん積まれていくのではないかと思われます。

「どこまでコードを実行したか」の情報は今回 1 パターンしかないので、これから実装するコードスタックもどきにはこの情報は要らなそうです。
posnum の情報だけ積んでみます。

combinatorics/perm.go
type callStackItem0 struct {
	pos          int // 順列の何桁目について扱っているか
	chosenNumber int // 順列の該当桁にどの数字を入れているか。上で言う`num`。
}

type callStack0 struct {
	last *callStack0Node
}

type callStack0Node struct {
	parent *callStack0Node
	value  *callStackItem0
}

func newCallStack0() *callStack0 {
	return &callStack0{nil}
}

func (s *callStack0) push(elem *callStackItem0) {
	node := callStack0Node{s.last, elem}
	s.last = &node
}

func (s *callStack0) pop() *callStackItem0 {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func (s *callStack0) peek() *callStackItem0 {
	return s.last.value
}

func (s *callStack0) empty() bool {
	return s.last == nil
}

func PermutationsWithStack0(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	callStack := newCallStack0()
	pattern := make([]int, k)

	callStack.push(&callStackItem0{pos: 0, chosenNumber: -1})
	for !callStack.empty() {
		// この1ループ内では、このコールスタックアイテムが担当する桁について
		// 値を増やすことを試みる
		env := callStack.peek()

		// 該当桁が右端を超えている時は、完成した順列をコールバック関数に渡し、
		// この桁については終了
		if env.pos == k {
			f(pattern)

			callStack.pop()
			continue
		}

		// 該当桁についてチェックリストの使用済みチェックを外しておく
		if env.chosenNumber > -1 {
			checklist[env.chosenNumber] = false
		}

		// 値増やしチャレンジ
		willContinue := false // 大域脱出用
		for env.chosenNumber++; env.chosenNumber < n; env.chosenNumber++ {
			// 使おうとする数字が使用済みならスキップ
			if checklist[env.chosenNumber] {
				continue
			}

			// 使用可能な数字を無事見つけたケース
			// 見つけた数字で順列を書き換え
			pattern[env.pos] = env.chosenNumber
			checklist[env.chosenNumber] = true

			// 1つ右の桁担当のコールスタックアイテムを積む。再帰呼び出しに相当。
			newEnv := callStackItem0{
				pos: env.pos + 1, chosenNumber: -1,
			}
			callStack.push(&newEnv)
			willContinue = true // 大域脱出
			break
		}
		// 大域脱出
		if willContinue {
			continue
		}

		// 値を増やすのに失敗したケース
		// コールスタックを1つ掘り下げて、1つ左の桁に戻って考え直す
		callStack.pop()
	}
}
PermutationsRecursive6	 191816791 ns	        96 B	       2 allocs
PermutationsWithStack0	1004648553 ns	 315651624 B	19728208 allocs

比較対象の PermutationsRecursive6 は前回記事での優勝記録です。全然勝てない…。

「各桁に今どの数字を入れているか」の情報を、コールスタックアイテムから省く

各桁に今どの数字を入れているかは、生成中の順列を見れば分かります。

combinatorics/util.go
type IntStack struct {
	last *intStackNode
}

type intStackNode struct {
	parent *intStackNode
	value  int
}

func NewIntStack() *IntStack {
	return &IntStack{nil}
}

func (s *IntStack) Push(elem int) {
	node := intStackNode{s.last, elem}
	s.last = &node
}

func (s *IntStack) Pop() int {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func (s *IntStack) Peek() int {
	return s.last.value
}

func (s *IntStack) Empty() bool {
	return s.last == nil
}
combinatorics/perm.go
func PermutationsWithStack1(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	posStack := NewIntStack() // コールスタックアイテムが`pos`のみになった
	pattern := make([]int, k)
	// 生成中の順列には、「まだちゃんと値を入れていない」という情報を
	// ちゃんと-1として埋め込む必要が出てきた
	for i := range pattern {
		pattern[i] = -1
	}

	posStack.Push(0)
	for !posStack.Empty() {
		pos := posStack.Peek()

		if pos == k {
			f(pattern)

			posStack.Pop()
			continue
		}

		chosenNumber := pattern[pos] // 生成中の順列から値取得
		if chosenNumber > -1 {
			checklist[chosenNumber] = false
		}

		willContinue := false
		for chosenNumber++; chosenNumber < n; chosenNumber++ {
			if checklist[chosenNumber] {
				continue
			}

			pattern[pos] = chosenNumber
			checklist[chosenNumber] = true

			posStack.Push(pos + 1)
			willContinue = true
			break
		}
		if willContinue {
			continue
		}

		pattern[pos] = -1 // ちゃんと-1にリセットしないといけない
		posStack.Pop()
	}
}
PermutationsRecursive6	 191816791 ns	        96 B	       2 allocs
PermutationsWithStack0	1004648553 ns	 315651624 B	19728208 allocs
PermutationsWithStack1	 572861294 ns	 157825764 B	 9864104 allocs

再帰には相変わらず勝てる気がしませんが、先の実装と比べると、やたら速くなりました。
構造体に比べて int の方が遥かに軽く扱えるということなんでしょうか。

樹形図のノードをスタックに積む

ここまでのやり方は、「for 文開始 -> ある桁の値をちょっとだけ増やす -> break(次のスタックアイテムへ) -> 戻ってきたら続きからまた for 文開始 -> ちょっとだけ増やしたらまた break」みたいな感じでした。
例えば「3 桁目に 1 を入れる -> 3 を入れる -> 4 を入れる」の場合、「for -> 1 -> break -> for -> 3 -> break -> for -> 4 -> break」という感じです。
このちまちま何度も for 文を使う感じ、なんかすごい無駄な気がします。

さて、DFS についてたとえば Wikipedia を調べてみると、どうやらスタックの使い方が違うことが分かります。
木構造の各ノードをスタックに積むのが普通みたいですね。
こっちのやり方なら、「for -> 4 をスタックに積む -> 3 をスタックに積む -> 1 をスタックに積む -> break」と一回の for 文実行でひと通り済ませることができます。
言葉で言っても分かりづらそうなので、以下が実際のコード;

combinatorics/perm.go
type patternNode2 struct {
	pos    int // 順列の何桁目か
	number int // 該当桁の数字
}

type patternNodeStack2 struct {
	last *patternNodeStack2Node
}

type patternNodeStack2Node struct {
	parent *patternNodeStack2Node
	value  *patternNode2
}

func newPatternNodeStack2() *patternNodeStack2 {
	return &patternNodeStack2{nil}
}

func (s *patternNodeStack2) push(elem *patternNode2) {
	node := patternNodeStack2Node{s.last, elem}
	s.last = &node
}

func (s *patternNodeStack2) pop() *patternNode2 {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func PermutationsWithStack2(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	patternNodeStack := newPatternNodeStack2()
	pattern := make([]int, k)

	patternNodeStack.push(&patternNode2{pos: -1, number: 0})
	for !patternNodeStack.empty() {
		patternNode := patternNodeStack.pop()

		// 以前の順列のノードに関する後始末をここで行っている
		for i := patternNode.pos; i < k; i++ {
			if i > -1 && pattern[i] > -1 {
				checklist[pattern[i]] = false
				// `pattern`もリセットしておかないと`checklist`のリセット時に
				// 狂いが生じる。
				pattern[i] = -1
			}
		}

		// ノード通りに順列を書き換え
		if patternNode.pos > -1 {
			pattern[patternNode.pos] = patternNode.number
			checklist[patternNode.number] = true
		}

		// 該当桁が右端の場合、完成した順列をコールバック関数に渡し、
		// この桁については終了
		if patternNode.pos == k-1 {
			f(pattern)
			continue
		}

		// 1つ右の桁担当のコールスタックアイテムをひと通り積む
		for num := n - 1; num >= 0; num-- {
			if checklist[num] {
				continue
			}

			childNode := patternNode2{
				pos: patternNode.pos + 1, number: num,
			}
			patternNodeStack.push(&childNode)
		}
	}
}

以前は peek したり pop したりだったのが、今回はひたすら pop するだけなので、その辺も分かりやすくなったと思います。

一方で、ノードを扱った後の後始末(チェックリストのリセット)のタイミングが「次の順列についてのノードを取り扱い始めた時の最初」という歪なタイミングになってしまい、このへんは分かりづらくなりました。
「1 ノード : 1 スタックアイテム : 1 処理」が対応している形なので、ノードを一度移ってしまったらもう元のノードに帰ってくるタイミングが無く、こうなっています。

PermutationsWithStack0	1004648553 ns	 315651624 B	19728208 allocs
PermutationsWithStack1	 572861294 ns	 157825764 B	 9864104 allocs
PermutationsWithStack2	 843198178 ns	 315651816 B	19728210 allocs

パフォーマンスはまあまあですね。

ポインタではなく値でスタックアイテムを持つ

上記のコードで、各スタックアイテムは一度作ったら特に値を書き換えることなく使い捨てるようになりました。
この場合、スタックアイテムが参照(ポインタ)である必要はありません。
値にしたらパフォーマンスがどう変わるか見てみましょう。

combinatorics/perm.go
type patternNode3 struct {
	pos    int
	number int
}

type patternNodeStack3 struct {
	last *patternNodeStack3Node
}

type patternNodeStack3Node struct {
	parent *patternNodeStack3Node
	value  patternNode3 // ポインタから値になった
}

func newPatternNodeStack3() *patternNodeStack3 {
	return &patternNodeStack3{nil}
}

// 引数`elem`がポインタから値になった
func (s *patternNodeStack3) push(elem patternNode3) {
	node := patternNodeStack3Node{s.last, elem}
	s.last = &node
}

// 返り値がポインタから値になった
func (s *patternNodeStack3) pop() patternNode3 {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func (s *patternNodeStack3) empty() bool {
	return s.last == nil
}

// `patternNode3`を使うようになったこと以外は、
// `PermutationsWithStack2`とほぼ同じ。
func PermutationsWithStack3(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	patternNodeStack := newPatternNodeStack3()
	pattern := make([]int, k)

	patternNodeStack.push(patternNode3{pos: -1, number: 0})
	for !patternNodeStack.empty() {
		patternNode := patternNodeStack.pop()

		for i := patternNode.pos; i < k; i++ {
			if i > -1 && pattern[i] > -1 {
				checklist[pattern[i]] = false
				pattern[i] = -1
			}
		}

		if patternNode.pos > -1 {
			pattern[patternNode.pos] = patternNode.number
			checklist[patternNode.number] = true
		}

		if patternNode.pos == k-1 {
			f(pattern)
			continue
		}

		for num := n - 1; num >= 0; num-- {
			if checklist[num] {
				continue
			}

			childNode := patternNode3{
				pos: patternNode.pos + 1, number: num,
			}
			patternNodeStack.push(childNode)
		}
	}
}
PermutationsWithStack1	 572861294 ns	 157825764 B	 9864104 allocs
PermutationsWithStack2	 843198178 ns	 315651816 B	19728210 allocs
PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs

結構速くなりました。

"-1桁目"から始めるのをやめる

だんだん

if patternNode.pos > -1 {
}

みたいな細かい if 文が増えてきて気持ち悪くなってきました。
どうやら -1 桁目(実際には存在せず、実装の都合で設けている)で始めているのが、今の実装と相性が悪そうです。
おとなしく 0 桁目から始めてみます。

combinatorics/perm.go
func PermutationsWithStack4(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	patternNodeStack := newPatternNodeStack3()
	pattern := make([]int, k)

	// エッヂケースとして別途処理しなきゃいけなくなった
	if k == 0 {
		f(pattern)
		return
	}

	// 0桁目のノード準備。-1桁目と違いn個分ある。
	for num := n - 1; num >= 0; num-- {
		patternNodeStack.push(patternNode3{pos: 0, number: num})
	}

	for !patternNodeStack.empty() {
		patternNode := patternNodeStack.pop()

		for i := patternNode.pos; i < k; i++ {
			if pattern[i] > -1 { // 条件減った
				checklist[pattern[i]] = false
				pattern[i] = -1
			}
		}

		// if文消えた
		pattern[patternNode.pos] = patternNode.number
		checklist[patternNode.number] = true

		if patternNode.pos == k-1 {
			f(pattern)
			continue
		}

		for num := n - 1; num >= 0; num-- {
			if checklist[num] {
				continue
			}

			childNode := patternNode3{
				pos: patternNode.pos + 1, number: num,
			}
			patternNodeStack.push(childNode)
		}
	}
}

ちょっとすっきりした箇所もあればちょっと煩雑になった箇所もあり、という感じ。イマイチですね。

PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack4	 663686560 ns	 315651548 B	 9864106 allocs

パフォーマンスは当然大して変わらないです。

チェックリスト式から整数配列式に戻す

ノードの後始末なんてしなきゃいけないからこんなややこしいコードになるんや!
なんで後始末が必要なんや!
チェックリストを管理しなきゃいけないからや!
というわけで、ここでチェックリストをやめてみます。
使用可能な数字群の管理には、代わりに元の整数配列を使うことにします。

combinatorics/perm.go
type patternNode5 struct {
	pos    int
	number int
	rest   []int // 樹形図の該当ノードより右のノードに使える、まだ使っていない数字群
}

type patternNodeStack5 struct {
	last *patternNodeStack5Node
}

type patternNodeStack5Node struct {
	parent *patternNodeStack5Node
	// 検証の結果、構造体の要素が1つ増えた今でも参照より値の方がパフォーマンスが良かった
	// (具体的な検証結果についてはだるいので省略)
	value  patternNode5
}

func newPatternNodeStack5() *patternNodeStack5 {
	return &patternNodeStack5{nil}
}

func (s *patternNodeStack5) push(elem patternNode5) {
	node := patternNodeStack5Node{s.last, elem}
	s.last = &node
}

func (s *patternNodeStack5) pop() patternNode5 {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func (s *patternNodeStack5) empty() bool {
	return s.last == nil
}

func PermutationsWithStack5(a []int, k int, f func([]int)) {
	patternNodeStack := newPatternNodeStack5()
	pattern := make([]int, k)

	patternNodeStack.push(patternNode5{pos: -1, number: 0, rest: a})
	for !patternNodeStack.empty() {
		patternNode := patternNodeStack.pop()

		if patternNode.pos > -1 {
			pattern[patternNode.pos] = patternNode.number
		}

		if patternNode.pos == k-1 {
			f(pattern)
			continue
		}

		for i := len(patternNode.rest) - 1; i >= 0; i-- {
			// i番目の項目を抜いた配列を新しく作る
			newRest := make([]int, len(patternNode.rest)-1)
			for j := 0; j < i; j++ {
				newRest[j] = patternNode.rest[j]
			}
			for j := i + 1; j < len(patternNode.rest); j++ {
				newRest[j-1] = patternNode.rest[j]
			}

			childNode := patternNode5{
				pos:    patternNode.pos + 1,
				number: patternNode.rest[i],
				rest:   newRest,
			}
			patternNodeStack.push(childNode)
		}
	}
}

ちょっとスッキリして分かりやすくなったんじゃないでしょうか。

PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack5	 969391196 ns	 557476872 B	16099412 allocs

代わりにパフォーマンスは悪化しました。
やはり我々はもうチェックリスト無しでは行きていけないのだ…。

生成中の順列配列から残りの使用可能な数字を判断する

チェックリストに代わる策をめげずにもう 1 つ試してみます。

combinatorics/perm.go
func PermutationsWithStack6(n, k int, f func([]int)) {
	patternNodeStack := newPatternNodeStack3()
	pattern := make([]int, k)

	patternNodeStack.push(patternNode3{pos: -1, number: 0})
	for !patternNodeStack.empty() {
		patternNode := patternNodeStack.pop()

		if patternNode.pos > -1 {
			pattern[patternNode.pos] = patternNode.number
		}

		if patternNode.pos == k-1 {
			f(pattern)
			continue
		}

		for num := n - 1; num >= 0; num-- {
			// 数字が使われていたらスキップ
			willContinue := false // 大域脱出用
			for i := 0; i <= patternNode.pos; i++ {
				if pattern[i] == num {
					willContinue = true // 大域脱出
					break
				}
			}
			// 大域脱出
			if willContinue {
				continue
			}

			childNode := patternNode3{
				pos: patternNode.pos + 1, number: num,
			}
			patternNodeStack.push(childNode)
		}
	}
}
PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack5	 969391196 ns	 557476872 B	16099412 allocs
PermutationsWithStack6	1170586330 ns	 315651632 B	 9864109 allocs

うん。駄目ですね。

操作内容を構造体で表してスタックに積む

ここまで問題としてきたのは、「スタックに沿って樹形図のノードを処理していくと、ノードを処理したあと(そこからもっと右に生えていくノードも全部処理したあと)の後始末をしたい時に、そのノードを表すスタックアイテムがもう無くなっているので、次のノードに移ってから無理やり前のノードについて後始末しなければいけなくなり、コードが煩雑になる」という点でした。

解決策としてここまで「後始末の必要性をなくせばいいのでは?」と取り組んできたけれども、どれもパフォーマンスを犠牲にする結果に。

今度は別の解決策として、「じゃあ後始末のためのスタックアイテムも積んでおけばいいじゃない」というのを試してみます。

この場合、スタックアイテムにはノードの情報だけでなく「通常処理をすべきか後始末をすべきか」の情報が必要になってきます。
こうなってくると、スタックアイテムが指すものはノード自体というよりも、「このノードをこうしろ」という「操作内容」の方が近いです。

combinatorics/perm.go
const (
	// 操作の種類
	operationMode7ReflectValue = iota // 通常処理1
	operationMode7ExecuteOrDelegate   // 通常処理2
	operationMode7ResetValue          // 後始末
)

type operation7 struct {
	pos    int // 該当ノードについて、順列の何桁目か
	number int // 該当ノードについて、該当桁の数字
	mode   int // 操作の種類
}

type operationStack7 struct {
	last *operationStack7Node
}

type operationStack7Node struct {
	parent *operationStack7Node
	value  operation7
}

func newOperationStack7() *operationStack7 {
	return &operationStack7{nil}
}

func (s *operationStack7) push(elem operation7) {
	node := operationStack7Node{s.last, elem}
	s.last = &node
}

func (s *operationStack7) pop() operation7 {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func (s *operationStack7) empty() bool {
	return s.last == nil
}

func PermutationsWithStack7(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	operationStack := newOperationStack7()
	pattern := make([]int, k)

	operationStack.push(operation7{
		pos:    -1,
		number: 0,
		mode:   operationMode7ExecuteOrDelegate,
	})
	for !operationStack.empty() {
		operation := operationStack.pop()

		switch operation.mode {
		case operationMode7ReflectValue:
			pattern[operation.pos] = operation.number
			checklist[operation.number] = true

		case operationMode7ExecuteOrDelegate:
			if operation.pos == k-1 {
				// Execute
				f(pattern)
			} else {
				// Delegate
				for num := n - 1; num >= 0; num-- {
					if checklist[num] {
						continue
					}

					// スタックなので、先に入れた操作ほど後に実行されることに注意
					operationStack.push(operation7{
						pos:    operation.pos + 1,
						number: num,
						mode:   operationMode7ResetValue,
					})
					operationStack.push(operation7{
						pos:    operation.pos + 1,
						number: num,
						mode:   operationMode7ExecuteOrDelegate,
					})
					operationStack.push(operation7{
						pos:    operation.pos + 1,
						number: num,
						mode:   operationMode7ReflectValue,
					})
				}
			}

		case operationMode7ResetValue:
			checklist[operation.number] = false
		}
	}
}

通常処理と後始末を分けるついでに、通常処理も 2 つに分けることにより、"-1 桁目"について通常処理 1(順列への値の反映)をスキップしなければいけない部分も綺麗に書けました。
冗長な気もしますが、個人的には他のパターンより書きやすかったです。

PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack5	 969391196 ns	 557476872 B	16099412 allocs
PermutationsWithStack6	1170586330 ns	 315651632 B	 9864109 allocs
PermutationsWithStack7	1766143475 ns	 946954664 B	29592321 allocs

しかしパフォーマンスは悪くなる一方。

いっそ操作を関数で表してスタックに積む

操作ってそれもう関数じゃ〜ん、と思ったので、関数自体をスタックに積めないか試してみました。

combinatorics/util.go
type FuncStack struct {
	last *funcStackNode
}

type funcStackNode struct {
	parent *funcStackNode
	value  func()
}

func NewFuncStack() *FuncStack {
	return &FuncStack{nil}
}

func (s *FuncStack) Push(elem func()) {
	node := funcStackNode{s.last, elem}
	s.last = &node
}

func (s *FuncStack) Pop() func() {
	value := s.last.value
	s.last = s.last.parent
	return value
}

func (s *FuncStack) Peek() func() {
	return s.last.value
}

func (s *FuncStack) Empty() bool {
	return s.last == nil
}
combinatorics/perm.go
func PermutationsWithStack8(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	operationStack := NewFuncStack()
	pattern := make([]int, k)

	reflectValue := func(pos, number int) {
		pattern[pos] = number
		checklist[number] = true
	}
	resetValue := func(number int) {
		checklist[number] = false
	}
	// 中で再帰的に自身を呼ぶので、宣言を先に済ませる必要がある
	var executeOrDelegate func(pos int)
	executeOrDelegate = func(pos int) {
		if pos == k-1 {
			f(pattern)
			return
		}

		for num := n - 1; num >= 0; num-- {
			if checklist[num] {
				continue
			}

			// 以降で作る関数の中で`num`を直接参照してしまうと、実際にその関数が実行されて
			// `num`を使う頃には、`num`の値がもうfor文によって書き換えられてしまっている、
			// という状態になってしまう。
			// そのためループ毎に新しい変数`numFrozen`を作り、そっちに値を退避させておく必要がある。
			numFrozen := num
			operationStack.Push(func() {
				resetValue(numFrozen)
			})
			operationStack.Push(func() {
				executeOrDelegate(pos + 1)
			})
			operationStack.Push(func() {
				reflectValue(pos+1, numFrozen)
			})
		}
	}

	operationStack.Push(func() {
		executeOrDelegate(-1)
	})
	for !operationStack.Empty() {
		operation := operationStack.Pop()
		operation()
	}
}

いやもう再帰しちゃってるじゃん!

PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack5	 969391196 ns	 557476872 B	16099412 allocs
PermutationsWithStack6	1170586330 ns	 315651632 B	 9864109 allocs
PermutationsWithStack7	1766143475 ns	 946954664 B	29592321 allocs
PermutationsWithStack8	3088975270 ns	1420431936 B	59184625 allocs

もうだめぽ。

まとめ

ここまでの計測結果を前回の優勝結果とともに全て並べてみると、

PermutationsRecursive6	 191816791 ns	        96 B	       2 allocs
PermutationsWithStack0	1004648553 ns	 315651624 B	19728208 allocs
PermutationsWithStack1	 572861294 ns	 157825764 B	 9864104 allocs
PermutationsWithStack2	 843198178 ns	 315651816 B	19728210 allocs
PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack4	 663686560 ns	 315651548 B	 9864106 allocs
PermutationsWithStack5	 969391196 ns	 557476872 B	16099412 allocs
PermutationsWithStack6	1170586330 ns	 315651632 B	 9864109 allocs
PermutationsWithStack7	1766143475 ns	 946954664 B	29592321 allocs
PermutationsWithStack8	3088975270 ns	1420431936 B	59184625 allocs

このようになり、結局再帰には全然勝てませんでした。
(少なくともGo の場合は)おとなしく再帰で書いた方が良い模様。
生半可な自作スタックより、本物のコールスタックの方が色々最適化されているんですかね。

「どうしても再帰が書けないからスタックを使って書きたい!」という人は、書きやすさとパフォーマンスのトレードオフを鑑みながら、どれか好きなのを選んでください。
個人的には構造体で操作を表すやつが好きです。

と思いきや!大事な追記

「スタックをスライスで実装したらどうなるの?」というツッコミが入ったので、試してみました。

// PermutationsWithStack0のスライス使った版
func PermutationsWithSlice0(n, k int, f func([]int)) {
	checklist := make([]bool, n)
	callStack := make([]callStackItem0, 1) // スライスに変更
	pattern := make([]int, k)

	callStack[0] = callStackItem0{pos: 0, chosenNumber: -1}
	for len(callStack) > 0 {
		env := &callStack[len(callStack)-1] // peek

		if env.pos == k {
			f(pattern)

			callStack = callStack[:len(callStack)-1] // pop(discard)
			continue
		}

		if env.chosenNumber > -1 {
			checklist[env.chosenNumber] = false
		}

		willContinue := false
		for env.chosenNumber++; env.chosenNumber < n; env.chosenNumber++ {
			if checklist[env.chosenNumber] {
				continue
			}

			pattern[env.pos] = env.chosenNumber
			checklist[env.chosenNumber] = true

			newEnv := callStackItem0{
				pos: env.pos + 1, chosenNumber: -1,
			}
			callStack = append(callStack, newEnv) // push
			willContinue = true
			break
		}
		if willContinue {
			continue
		}

		callStack = callStack[:len(callStack)-1] // pop(discard)
	}
}

// PermutationsWithSlice1以降も同様なので省略。
// また、PermutationsWithSlice3はPermutationsWithSlice2と同じなので除いた。
PermutationsRecursive6	 191816791 ns	        96 B	       2 allocs
PermutationsWithStack0	1004648553 ns	 315651624 B	19728208 allocs
PermutationsWithStack1	 572861294 ns	 157825764 B	 9864104 allocs
PermutationsWithStack2	 843198178 ns	 315651816 B	19728210 allocs
PermutationsWithStack3	 648371094 ns	 315651576 B	 9864106 allocs
PermutationsWithStack4	 663686560 ns	 315651548 B	 9864106 allocs
PermutationsWithStack5	 969391196 ns	 557476872 B	16099412 allocs
PermutationsWithStack6	1170586330 ns	 315651632 B	 9864109 allocs
PermutationsWithStack7	1766143475 ns	 946954664 B	29592321 allocs
PermutationsWithStack8	3088975270 ns	1420431936 B	59184625 allocs
PermutationsWithSlice0	 234075281 ns	       576 B	       6 allocs
PermutationsWithSlice1	 184274404 ns	       336 B	       6 allocs
PermutationsWithSlice2	 186323035 ns	      2112 B	       8 allocs
PermutationsWithSlice4	 183515108 ns	      2496 B	       6 allocs
PermutationsWithSlice5	 360493641 ns	  84004413 B	 6235310 allocs
PermutationsWithSlice6	 657643570 ns	      2096 B	       7 allocs
PermutationsWithSlice7	 183175502 ns	      9504 B	       9 allocs
PermutationsWithSlice8	2023963760 ns	 946958520 B	29592322 allocs

再帰相手にも勝ちまくれるようになりました。アイヤー。

Next: Golang による順列列挙のパフォーマンス研究 3. 繰り上がり法(仮名)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?