🐈

スーパーマーケットのシステムで学ぶコンピュータ用語

2021/07/31に公開

はじめに

世の中を観察していると「あー、これはコンピュータの世界でもよく見るやつだなあ」と思うことがよくあります。本記事の目的はスーパーマーケット(以降"スーパー"と記載)のシステムを用いて、たとえ話によってコンピュータシステムの用語を理解をしやすくすることです。

本記事で紹介するスーパーマーケットの用語とコンピュータの用語は正確に一対一に対応しているわけではなく、かつ、比喩に頼りすぎると誤った理解につながります。ぜひ本記事で紹介している概念について興味が生まれたら記事を読み終わった後にご自身で定義を確認してみてください。

処理の並列化

スーパーのお客さんは買い物が終わったらさっさとレジを通して帰宅したいはずです。このため、レジ処理を待たせて「いつまで待たせるんだ、もうここには二度と来ない」と思われるのはうれしくないです。これを避けるために、とくにそれなりの規模の店舗では、レジ待ちの人数を減らすためにレジを複数台用意して処理して、レジ処理を並列実行できるようにしています。

コンピュータの世界でレジをCPUコアに、お客さんを店員さんをプロセス(あるいはスレッド)に、そしてお客さんをリクエストだと見立ててみましょう。そうすると店員さんがいるレジが複数あれば複数人のお客さんに対応できるように、CPUコアとプロセスが複数あればリクエスト処理を並列化できることがわかります。

もう一点、レジがいくらあってもそこに立つ店員さんがいなければお客さん対応はできないのと同様、CPUコア数を増やすだけではなく同時に動かすプロセスの数を増やしてはじめて処理が並列化できることがわかります。

並列度の動的な変動

スーパーではレジが複数あっても全部のレジに常に店員さんが立っているわけではありません。なぜならお客さんがいないときにレジに店員さんが立っていてもなにもできず[1]、雇うのにお金がかかる店員さんの貴重なリソースが無駄になるからです。スーパーにはレジ打ち以外にも仕事はいくらでもあるので経営者からするとレジ打ちの仕事がないときには別の仕事をしてもらいたいのです。

このときスーパーでは以下のような仕組みを使って店員さんというリソースを効率的に利用します。

  1. 普段はレジを一部(たとえば1台)しか開けない
  2. 支払いを待っているお客さんがいたらレジに立っている店員さんが店内にレジ応援を依頼する業務連絡を流して(よくあるのが「レジ応援お願いします」とかいうもの)、それを聞いた他の店員さんがやってきて別のレジを開ける
  3. レジ待ちのお客さんがいなくなったら、また受付可能なレジの台数を減らす

これによってお客さんの待ち時間を短くしつつ、かつ、リソースを遊ばせないようにできます。

コンピュータの世界でもプログラムが負荷に応じてリクエストを処理するワーカースレッド数を増減させるようなことをします。たとえば以下のGo言語で書かれたプログラムはlocalhost:8080に接続してきたクライアントにhtmlを返すのですが、個々のリクエストに対してgoroutineというワーカースレッドのようなものを生成することによってコア数が許す限り並列にリクエストを処理できるようになっています[2]

package main

import (
	"fmt"
	"log"
	"net/http"
)

func HandleTopPage(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "<html><body><p>hello</p></body></html>\n")
}

func main() {
	http.HandleFunc("/", HandleTopPage)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal(err)
	}
}

CPUのパイプライン処理

レジでの作業は雑にいうと(a)お客さんから受け取った買い物の価格を計算して(b)代金を受け取るという2つです。店舗によってはこの二つの工程にそれぞれ店員さんを割り当てていることがあります。これによってお客さんをさばく速度が上げられるのです。

たとえば3人のお客さんC0, C1, C2がレジに並んでいるとして、かつ、(a)(b)にそれぞれ1分かかるとします。工程を分割していない場合は次のようになります。

  1. 開始時: C0の処理始まり
  2. 2分後: C0の処理終わり、C1の処理始まり
  3. 4分後: C1の処理終わり、C2の処理始まり
  4. 6分後: C2の処理終わり

いっぽう工程を分割する場合は次のようになります。

  1. 開始時: C0の処理(a)始まり
  2. 1分後: C0の処理(b)始まり、C1の処理(a)始まり
  3. 2分後: C0の処理(b)終わり、C1の処理(b)始まり、C2の処理(a)始まり
  4. 3分後: C1の処理(b)終わり、C2の処理(b)始まり
  5. 4分後: C2の処理(b)終わり

このバケツリレーのようなしくみによって2人のお客さんのレジ処理にかかる時間が短くなりました。お客さん個々人から見ると処理全体にかかる時間は変わりませんが、処理が始まるまでの時間が短くなったのでストレスが軽減することでしょう。

コンピュータにはこれに似たしくみがたくさんあります。代表的なもののひとつはCPUのパイプライン処理です。CPUによる命令の実行には以下のように複数の処理が必要です(もっと細かく分けることもあります)。

  1. 命令をメモリから読み出す(fetch)
  2. 命令の種類を解釈する(decode)
  3. 命令を実行する(execute)
  4. 命令の実行結果をCPUのレジスタに書き込む(writeback)

これら工程をCPUの中の別のユニットで実行し、かつ、個々の工程は並列に実行できるようにすることで性能向上を図るのがパイプライン処理です。

2つの命令AとBをを順番に実行する場合のコストを見てみましょう。
fetch,decode,execute,writebackにそれぞれ1ナノ秒かかるものとします。

まずはパイプライン処理のしくみが存在しない場合の処理です。

  1. 開始時: Aのfetch開始
  2. 1ナノ秒後: Aのdecode開始
  3. 2ナノ秒後: Aのexecute開始
  4. 3ナノ秒後: Aのwriteback開始
  5. 4ナノ秒後: Aの処理終了,Bのfetch開始
  6. 5ナノ秒後: Bのdecode開始
  7. 6ナノ秒後: Bのexecute開始
  8. 7ナノ秒後: Bのwriteback開始
  9. 8ナノ秒後: Bの処理終了

これに対してパイプライン処理をサポートしている場合は次のようになります。

  1. 開始時: Aのfetch開始
  2. 1ナノ秒後: Aのdecode開始, Bのfetch開始
  3. 2ナノ秒後: Aのexecute開始, Bのdecode開始
  4. 3ナノ秒後: Aのwriteback開始, Bのexecute開始
  5. 4ナノ秒後: Aの処理終了,Bのwriteback開始
  6. 5ナノ秒後: Bの処理終了

パイプライン処理をすることによって2命令の合計所要時間を8ナノ秒から5ナノ秒に短縮できました。

パイプラインはいいことづくめのようですが、命令の実行が途中で失敗するような場合はパイプラインハザードという問題が起きて一気に性能が落ちることもあります。これについてはスーパーのしくみで例えるのは無理があったので紹介は避けますが、興味があれば調べてみてください。本記事によってパイプライン処理の基本が理解できていればハザードの理解も容易になっているかと思います。

おわりに

本記事によって記事内で紹介したコンピュータの世界の概念への理解が少しでも深まったのであれば嬉しいです。

本記事で述べたものに限らず、世の中にあるものはお互いに似ていることがたくさんあります。コンピュータの世界の中でもそれは同様です。よろしければ過去記事のコンピュータ技術の「似たようなものは続くよどこまでも」も読んでみてください。

脚注
  1. レジでやるレジ打ち以外の仕事(レジに人がいるのに「レジ休止中」になってるときに店員さんがやっていること)については簡単のために省略します。 ↩︎

  2. コア数以上の並行処理はできません。 ↩︎

Discussion