読者です 読者をやめる 読者になる 読者になる

kakts-log

programming について調べたことを整理していきます

初 OSS contribution

github.com

nodeのnode-csvtojson パッケージへのPRマージされた なにげに初OSS contirubutionだったので記念にメモ

詳解システムパフォーマンス第6章 CPUアーキテクチャのまとめ

os linux cpu テクノロジー

詳解 システム・パフォーマンス

詳解 システム・パフォーマンス

最近発売された「詳解システムパフォーマンス」を買って読み進めています 第6章のCPUアーキテクチャの項目が勉強になったので、簡単にまとめます。

CPUの基礎用語

  • プロセッサ
    プロセッサボードのソケットなどに装着される物理チップ。 一つ以上のCPUをもつ
  • コア マルチコアプロセッサに含まれる、独立したCPUインスタンス CMP(chip level multi processing)と呼ばれるprocessorのスケーリング方法として使える
  • ハードウェアスレッド 1コアの上で複数スレッドの並列実行をサポートするCPUアーキテクチャのこと。各スレッドは独立したCPUインスタンスとして扱われる。 マルチスレッディングはこのスケーリングアプローチの内の一つ
  • CPU命令
    CPU命令セットに含まれる1個のCPUオペレーション
  • 論理CPU
    仮想プロセッサとも呼ばれる。 OSのCPUインスタンスであり、ハードウェアスレッド、コア、シングルコアプロセッサのどれかで実装される

CPU architecture

4コア、8ハードウェアスレッドをもつシングルプロセッサを考えた場合、以下のように図で表せる。
f:id:kakts:20170309235424j:plain

ハードウェアスレッドは、それぞれ1個の論理CPUとして扱うことができるため、OSからこのシングルプロセッサを見た時、8個のCPUを持っているように見える。 OSはさらにスケジューリングの最適化を行うために、どのCPUがどのコアに乗っているかといった情報を持っている場合がある。

CPU memory cache

プロセッサには様々なハードウェアキャッシュを持っており、メモリI/Oのパフォーマンス向上のために有用である。
キャッシュのサイズとI/Oスピードはトレードオフの関係にあり、高速になるほどCPUの近くに位置する。 キャッシュには、大きく分けて2種類あり、プロセッサの中、外にあるかで分けられる。
プロセッサ内にあるものは統合キャッシュと呼ばれることがあり、現在では主流となっている。

CPU run queue

スレッドには2つの実行状態があり、CPU上で実行中の場合と、CPUでの処理を待っている実行可能状態がある。
処理待ちのスレッドは、カーネルスケジューラにより、CPUのランキューにキューイングされ、順番にCPUの処理を待つ。
ランキューにキューイングされたスレッドの数は、CPUの飽和状態を示す重要なパフォーマンス指標となる。

マルチプロセッサシステムにおいて、カーネルはCPUそれぞれにランキューをもち、スレッドを同じランキューにキューイングし続けようとする。
なぜ同じランキューを選ぶかというと、CPUはスレッドのデータをキャッシュするため、パフォーマンスが良くなるからである。
この、スレッドに対して特定のランキューを選ぶアプローチをCPUアフィニティ と呼ぶ。

Goのスライスについて

go golang

Goのスライスについて

Go Slices: usage and internals - The Go Blog
Go言語において固定長のサイズの配列とは別に、要素の追加に応じて自由にサイズを拡張できるスライスという型があります。

スライスのデータ構造

スライスのデータ構造としては、ソースコードを読んでみるのが早くて、以下のurlにまとまっています。 https://github.com/golang/go/blob/master/src/runtime/slice.go#L11-L15

type slice struct {
    array unsafe.Pointer // 配列のポインタ
    len   int   // スライスが現在もっている要素数
    cap int    // スライスのサイズ
}

スライスの内部的には、配列のポインタを持っているため、従来の配列と同様の操作が行えます。

さらには、現在スライスが現在持っている要素数lenをもっていて、要素を追加するたびにこの値が増加していきます。 lenとは別に、スライスの宣言時に初期化した要素に応じてcapも決められます。 それと同時に、lenとcapをチェックし、capの値を超えたら、古いcapサイズの2倍のサイズのメモリを確保(malloc)し、同じ要素でサイズが2倍のスライスを新たに作ります。

このあたりの処理はsliceの内部実装で使われているgrowsliceという関数で実現されています。 go/slice.go at master · golang/go · GitHub
読んでみると、スライス拡張の際に新たにメモリサイズを計算し、mallocして新たにメモリが割り当てられていることが理解できると思います。

スライスの宣言

スライスを使う際に、宣言する方法は配列のものと非常に似ていて、下記のように書くことができます。 makeを使ってcapとlenを指定した上で宣言する事もできます。

// 配列の宣言
// 宣言時に[]にサイズを指定する。固定長のため、サイズが増えることはない
arr := [4]int{1, 2, 3, 4}


// スライスの宣言
// サイズは指定せず、4つの要素を指定して初期化している。
sl := []int{1, 2, 3, 4}

// makeを使って、サイズを決めた上で宣言も行える
sl := make([]int, 4, 8)

スライスの要素の操作

スライスは宣言時に自由に要素を指定することができ、指定した要素に応じて長さが決まることを説明しました。

このスライスに対して、様々な操作を行うことができます。

sl := []int{1, 2, 3, 4}
// 元のスライスの1番目から(4-1)番目の要素をもつスライスを取得
fmt.Println(sl[1:4]) //  [2 3 4]と表示
// 元のスライスの0番目から(2-1)番目の要素をもつスライスを取得
fmt.Println(sl[:2]) // [1 2]と表示

そして、もちろんスライスの特定の要素に対して値を代入することもできます。

sl := []int{1, 2, 3, 4}
sl[2] = 100
fmt.Println(sl) // [1 2 100 4]と表示

また、別の変数に対して、既に宣言したスライスの参照を渡すことができます。

sl := []int{1, 2, 3, 4}

// sl2にslの参照を渡す
sl2 := sl

fmt.Println(sl) // [1 2 3 4]と表示
fmt.Println(sl2) // [1 2 3 4]と表示

// slの特定の要素を変更すると、同じ参照を持つsl2の方でも値が変わっているのを確認できる。
sl[2] = 100
fmt.Println(sl) // [1 2 100 4]と表示
fmt.Println(sl2) // [1 2 100 4]と表示

上記のように、別の変数に参照を渡すことで、同じ実体を複数の変数から見ることができます。

appendを用いたスライスの要素の追加

既に宣言済みのスライスに対して、要素を追加する際にはappend()関数を使って行います。 ここでは、len, capが4のスライスに対して、appendを行ってみます。

sl := []int{1, 2, 3, 4}

// sl: len 4  cap 4   slは [1 2 3 4]
fmt.Println("sl: len, %d cap %d", len(sl), cap(sl), sl)

// len,capが4で要素が満杯の状態でappendを行う
sl3 = append(sl, 5)
// sl: len 5  cap 8       sl は [1 2 3 4 5]
fmt.Printf("sl: len, %d cap %d", len(sl), cap(sl))

既に要素が満杯のスライスに対して appendを行うので、前述したとおり、新たにcapサイズを2倍の8にした上で、5を追加したスライスを返しています。
内部的にはmallocをしているので、append前と後では、変数slが指すポインタアドレスは別のものになっています。
スライスを使って実装をする上で、スライスのデータ構造、内部実装を理解していないと思わぬバグになりそうなので、ここは非常に注意が必要です。

Go deferについて

go go言語 defer

Goには、deferステートメントというものがあり、deferへ渡した関数実行を、その呼び出し元の関数の終了時(return)まで遅延させることができます。

package main

import "fmt"

func main() {
  // main関数の最後に実行される
  defer fmt.Println("world.")

  fmt.Println("hello.")
}

出力
hello. world

deferステートメントの特徴として、deferに関数を渡す際、引数の値は、deferに渡すタイミングで評価されます。
以下のようなコードを書いて実行してみると、その挙動がわかって面白いです

package main

import "fmt"

func main() {
  var i = 1

  // i = 1が fmt.Printlnに渡され、最後に実行される
  defer fmt.Println("world. %d", i)
  
  // i = 2
  i++
  //  i = 2が fmt.Printlnに渡され、最後に実行される
  defer fmt.Println("world2. %d", i)

  // i = 2が渡され、その場で実行される
  fmt.Println("hello %d", i)
}

出力
hello %d 2
world2. %d 2
world. %d 1

まとめると、deferに渡した関数は、引数はその場で評価され、実行は呼び出し元の関数の終了時に行われる。
ここの挙動について簡単ですが、しっかり理解していないと複雑なコードになったときにバグの元になるため注意が必要です。

Go 変数宣言についてのまとめ

Go プログラミング テクノロジー

仕事で Goを使う機会があったので、基本から勉強していて、変数宣言について気になった点についてメモがてらまとめます。

Golangにおける変数宣言において、 varをつかって変数宣言をするのが一般的

package main

import (
  "fmt"
)

func main() {

  // int型で宣言 初期値指定なし
  var i int

  // iとjをint型で宣言 初期値指定なし
  var i, j int

  // iとjをint型で宣言 初期値指定あり
  var i, j int = 1, 2

}

上記の方法だと変数毎に型を指定しましたが、 := をつかえば、型を指定せずに、暗黙的に変数宣言ができます。

package main

import "fmt"

func main() {
  var i, j int = 1, 2

  // := は暗黙の型変形
  k := 3
  c, python, java := true, false, "no !"

  // 1 2 3 true false no ! と表示される
  fmt.Println(i, j, k, c, python ,java)

  var i int = 1
  j := "1"  // string
  k := 1    // int

  // 変数型の確認
  fmt.Println(reflect.TypeOf(i))
  fmt.Println(reflect.TypeOf(j))
  fmt.Println(reflect.TypeOf(k))
}

ただし、:= をつかった暗黙的宣言は、基本的に関数スコープ内でのみの利用が可能で、関数宣言外では利用できません。

package main

import "fmt"

// 関数外での暗黙的宣言
l := 1
func main() {
  t := 1
  fmt.Println(t)
}

// コンパイルエラーが出る
$go run test-type.go
./test-type.go:7: syntax error: non-declaration statement outside function body