ゴルーチン内の乱数生成を高速に行う

TL;DR

  • math/rand パッケージのトップレベル関数(rand.Float64 関数など)では内部でロックを取得するので、複数のゴルーチンで乱数を生成するとパフォーマンスがでない
  • 複数のゴルーチンで乱数を生成するときは、Rand 構造体のインスタンスを使って乱数を生成するとよい

経緯

Goの勉強のためにGoRayTracerというレイトレーサーを作っています。こんな画像を生成するプログラムです。

f:id:LocaQ:20190321172935p:plain

一通り動くところまでできましたが、この画像(640 x 480)を生成するのに1163秒(約20分)かかります。

同じ画像を生成できるFRayTracerを前に作っていたのですが、それと比べて2倍以上の時間がかかっていました。 特別な最適化をしていない.NETのプログラムより遅いのはおかしいと思い、プロファイラーを使って原因を調べてみました。

プロファイリング&分析

プロファイリングにはpkg/profileというライブラリを使いました。

main関数の先頭に以下のようなコードを追加すると、関数ごとのCPU使用時間を取得できます。

func main() {
    defer profile.Start(profile.ProfilePath(".")).Stop()

     ...
}

ビルドし、プログラムを起動して実行が終わると cpu.pprof というファイルができるので、Goの pprof というツールを使って結果を分析します。

>go tool pprof .go\bin\go_raytracer.exe cpu.pprof
File: go_raytracer.exe
Type: cpu
Time: Mar 10, 2019 at 8:49pm (JST)
Duration: 4.63mins, Total samples = 29.65mins (640.59%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

top -cumと入力して時間のかかる関数を表示します。 -cumは「対象の関数だけではなく、その関数の呼び出し先まで含めた実行時間を表す"cumulative weight"の値でソートする」オプションのようです。 (flat は「対象の関数の実行時間から、呼び出し先の関数の実行時間を引いた時間」を表すようです。)

(pprof) top -cum
Showing nodes accounting for 14.34mins, 48.37% of 29.65mins total
Dropped 352 nodes (cum <= 0.15mins)
Showing top 10 nodes out of 73
      flat  flat%   sum%        cum   cum%
         0     0%     0%  26.07mins 87.93%  github.com/locatw/go-ray-tracer/rendering.(*RayTracer).renderPixelRoutine
  0.06mins  0.21%  0.21%  26.07mins 87.93%  github.com/locatw/go-ray-tracer/rendering.(*RayTracer).renderPixel
  0.36mins  1.21%  1.42%  25.22mins 85.08%  github.com/locatw/go-ray-tracer/rendering.(*RayTracer).traceRay
  0.04mins  0.13%  1.55%  15.99mins 53.94%  math/rand.Float64
  0.05mins  0.16%  1.70%  15.95mins 53.82%  math/rand.(*Rand).Float64
  0.03mins 0.087%  1.79%  15.91mins 53.66%  math/rand.(*Rand).Int63
  0.65mins  2.21%  3.99%  15.88mins 53.57%  math/rand.(*lockedSource).Int63
  0.30mins  1.01%  5.01%  15.77mins 53.20%  github.com/locatw/go-ray-tracer/element.CreateDiffuseRay
  2.29mins  7.73% 12.74%  13.09mins 44.17%  sync.(*Mutex).Lock
 10.56mins 35.63% 48.37%  10.56mins 35.63%  runtime.procyield
(pprof)

最初の3つはアプリのメソッドですが、その次に math/rand の関数とメソッドが4つ並んでいます。また、9番目に sync.(*Mutex).Lock があります。 この内、アプリで直接使っているのは math/rand.Float64 だけです。この乱数生成だけで実行時間の50%以上を使っていることが読み取れます。

また、sync.(*Mutex).Lock と math/rand.(*lockedSource).Int63 があるので、「math/rand.Float64 の内部でロックを取得していてそれに時間がかかっている」と予想して調べたところ、以下の記事を見つけました。

golangのrandパッケージのlockについて - DevDevデブ!!

この記事(とさらに参照先の記事)を読むと、やはり math/rand.Float64 などのトップレベル関数は内部でロックを取っているようです。 ロックを取っているのでパフォーマンスが出ない代わりに、ゴルーチンセーフになっているとのこと。 また、ゴルーチン毎に Rand 構造体のインスタンスを作成し、それを使って乱数を生成するとパフォーマンスがよくなると書かれています。

Rand 構造体を使って乱数を生成する

GoRayTracerではCPUコア数と同じだけのゴルーチンを生成し、内部で rand.Float64 関数を使っています。 なのでゴルーチンで実行する処理の内部で Rand 構造体のインスタンスを生成し、それを使って乱数を生成するようにしてみました。

optimize performance · locatw/GoRayTracer@c67a081

// ゴルーチン毎に生成
random := rand.New(rand.NewSource(time.Now().UnixNano()))

// ゴルーチンで実行される関数のなかで、Rand構造体のメソッドを使って乱数を生成する。
value := random.Float64()

変更前と変更後でベンチマークを取った結果が以下です。

コード 実行時間(秒)
改善前 1163
改善後 283

(CPU:Intel Core i7-8700, 画像の解像度:640 x 480)

変更前と比べて約4倍速くなりました。

プロファイリングでは乱数生成に全体の50%を使っていたので速くなるのは高々2倍くらいかと思いましたが、予想以上に速くなりました。 おそらく、プロファイラーは全ての処理の実行時間を計測しているわけではなくサンプリングしているため、実際の割合と差があり、結果予想以上に速くなったのだと思います。

Goの math/rand パッケージのソースコードを確認する

速くなって目的は達成しましたが、最後にGoのソースコードを確認してみます。 確認するコードはGo1.12の math/rand パッケージです。

go/rand.go at release-branch.go1.12 · golang/go

以下、ここからコードを引用します。全てのコードは引用しませんので詳しくは直接コードを見てください。

Float64、globalRand、lockedSource

まずは今回使っている、トップレベルの Float64 関数のコードを見てみます。

var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})

// Float64 returns, as a float64, a pseudo-random number in [0.0,1.0)
// from the default Source.
func Float64() float64 { return globalRand.Float64() }

トップレベルの Float64 関数は globalRand の Float64 メソッドを呼び出す実装になっています。 また、globalRand はパッケージプライベートな *Rand 型の変数です。

NewSource 関数は Source 型のオブジェクトを生成する関数、New 関数は新しい Rand 構造体のオブジェクトを生成する関数でした。

次にここまで出てきた Source 、Rand、lockedSource、Source と関連する Source64 の定義を見てみます。

// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {
    Int63() int64
    Seed(seed int64)
}

// A Source64 is a Source that can also generate
// uniformly-distributed pseudo-random uint64 values in
// the range [0, 1<<64) directly.
// If a Rand r's underlying Source s implements Source64,
// then r.Uint64 returns the result of one call to s.Uint64
// instead of making two calls to s.Int63.
type Source64 interface {
    Source
    Uint64() uint64
}

// A Rand is a source of random numbers.
type Rand struct {
    src Source
    s64 Source64 // non-nil if src is source64

    // readVal contains remainder of 63-bit integer used for bytes
    // generation during most recent Read call.
    // It is saved so next Read call can start where the previous
    // one finished.
    readVal int64
    // readPos indicates the number of low-order bytes of readVal
    // that are still valid.
    readPos int8
}

type lockedSource struct {
    lk  sync.Mutex
    src Source64
}

Source は乱数生成源を表すインターフェースのようで、このインターフェースに定義されている Int63 メソッドで実際に乱数を生成するようです。 また、Source64 は Source に Uint64 メソッドを追加したインターフェースのようです。

Rand 構造体は Source、Source64 型のインスタンスを保持する構造体、lockedSource はミューテックスと Source64 型のオブジェクトを持つ構造体です。 また、ここには載せていませんが、Rand 構造体と lockedSource 構造体は Source、Source64 インターフェースに定義されているメソッドを定義していますので、Source、Source64 インターフェースを実装した構造体になっています。

lockedSource を使った乱数生成

globalRand を生成するコードと、アプリでゴルーチン毎に生成した Rand 構造体のインスタンスを生成するコードを比べてみます。

globalRand を生成するコード

var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})

GoRayTracerでゴルーチン毎に生成するコード

random := rand.New(rand.NewSource(time.Now().UnixNano()))

違いは New 関数に渡す引数で、globalRand は lockedSource を渡していますが、アプリでは通常の NewSource 関数で生成した乱数生成源を渡しています。

さて、トップレベルの Float64 関数

func Float64() float64 { return globalRand.Float64() }

ですが、globalRand の Float64 メソッドを呼び出しています。

これは内部を辿っていくと Rand 構造体が持つ乱数生成源 src の Int63 メソッドを呼び出すのですが、globalRand の場合は lockedSource の Int63 メソッドになります。 そのコードは以下のようになっています。

func (r *lockedSource) Int63() (n int64) {
    r.lk.Lock()
    n = r.src.Int63()
    r.lk.Unlock()
    return
}

確かに乱数生成時にロックを使っていました。

コンセントのない玄関にGoogle Home Miniを設置する

経緯

年末にGoogle Home Miniが半額になっていたので玄関に置こうと思って買ったんですが、いざ設置しようとしたらコンセントがないことに気付いてちょっと困りました。

ただ、せっかく買ったのに使わないのはもったいないので、とりあえず洗面台のコンセントを使って動かしてました。

f:id:LocaQ:20190120232305j:plain:w800

f:id:LocaQ:20190120232344j:plain:w400

でも色々問題がありました。

  • ドライヤーを使う時にいちいち挿し変えないといけなくて面倒
  • ドライヤーのワット数が1200W、洗面台のコンセントの最大ワット数も1200Wなので、電源タップを使えない
  • 洗面所のドアを閉められない
  • 洗面所に出入りすると、ドアがコードに当たって転がってひっくり返った状態になったりする
  • 電源コードの長さがギリギリで、そのうち断線しかねない

どうにかして電源を確保できないかネットで調べていたら、電球のソケットを使ってコンセントを増やすアダプターを見つけました。

f:id:LocaQ:20190120232521j:plain:w400 f:id:LocaQ:20190120232553j:plain:w400

しかし、もう1つ問題ができました。玄関の天井のソケットが斜めになっていて、このままではアダプターを挿せません。

f:id:LocaQ:20190120232649j:plain:w400

こちらは向きを変えられるアダプターがあったのでそれを使います。

使ったもの

以下のものを用意しました。

  • Google Home Mini
  • 可変式ソケット(E17口金→E26口金)
  • コンセント付きの電球アダプター(E26口金用)
  • LED電球(E26口金)
  • 電源タップ

store.google.com

注意として、コンセント付き電球アダプターがE26用のものしかありませんでしたが天井のソケットがE17でした。なので可変式ソケットはE17→E26へ変換するものを買いました。電球もそれに合わせてE26口金のものを買いました。

また、可変式ソケットがLED専用だったため、電球もLED電球にしています。ついでに人感センサー付きのものにしました。

設置

アダプターなどを接続するとこんな風になります。

f:id:LocaQ:20190120233353j:plain:w400

これを天井のソケットに挿します。

f:id:LocaQ:20190120233421j:plain:w400

可変式ソケットの傾きが足らなくて少し斜めになっていますが、他に合う可変式ソケットが見つからなかったのでこのままです。

次にアダプターのコンセントに電源タップを挿して、電源タップをシューズボックスに置きます。

f:id:LocaQ:20190120233452j:plain:w400 f:id:LocaQ:20190120233508j:plain:w400

上のシューズボックスの扉の背と壁の間が広すぎず狭すぎず、ちょうどコードを挟めるくらいの間隔だったのでコードを固定できました。

見た目はあまりよくないですが、壁から垂れ下がっているコードは玄関側からは見えないですし、天井のコードはLED電球が結構眩しくてあまり見られないのでこのままにしています。

Google Home Miniをシューズボックスの上に置きます。

f:id:LocaQ:20190120233520j:plain:w800

これでドライヤーを使うたびにコンセントを抜き差しする必要もなくなり、洗面所のドアも閉められるようになりました。

Google Cloud IoT Coreに登録されている端末の一覧をC#で取得する

概要

Google Cloud IoT Coreに登録されている端末の一覧をC#で取得する方法をまとめます。

Cloud IoT Coreに端末を登録する

Quickstart を参考にClout IoT Coreを作成して、端末を作成します。この記事の範囲では以下を行えばよいです。

  • Before you begin
  • Create a device registry
  • Add a device to the registry
  • Clean up

今回はTestDevice1TestDevice2という2つの端末を作成しました。

f:id:LocaQ:20190117230922p:plain:w800

また、それぞれの端末のメタ情報にNameを追加しました。

f:id:LocaQ:20190117230953p:plain:w900

認証

以下のページを参考にサービスアカウントというものを作成します。

認証の概要

作成するアカウントは以下を選びました。

  • サービスアカウント:Compute Engineのデフォルトのサービスアカウント
  • キーのタイプ:JSON

f:id:LocaQ:20190117231023p:plain:w600

※もしかしたらCompute EngineVMを作成していないとデフォルトのサービスアカウントは出てこないかもしれません。

「作成」ボタンを押すとJSONファイルがダウンロードされます。

サンプルプログラム

コード

Cloud IoT Coreに登録されている端末の一覧と、各端末の情報を表示するサンプルプログラムです。

Google Cloud IoT Coreに登録されている端末の一覧を取得するプログラム

プログラム中のPROJECT_ID, REGION, REGISTRY_IDは各自置き換えます。PROJECT_IDはClout IoT Coreを作成したプロジェクトのID、REGIONus-central1など、REGISTRY_IDはCloud IoT CoreのレジストリIDです。

端末の情報として以下の2つを表示しています。

  • ID
  • メタ情報のName

cloudIotService.Projects.Locations.Registries.Devices.List(parent).ExecuteAsync()で取得できる端末は、IDなどの限られた情報のみ設定されているようで、メタ情報は取得できません。

そこで、さらに各端末毎にcloudIotService.Projects.Locations.Registries.Devices.Get(name).ExecuteAsync()を呼び出して詳細な端末情報を取得します。このメソッドで取得した端末情報にはメタ情報なども含まれています。

必要な設定

このプログラムは環境変数GOOGLE_APPLICATION_CREDENTIALSが必要です。値はサービスアカウントの作成でダウンロードしたJSONファイルのパスです。 この環境変数GoogleCredential.GetApplicationDefault()で参照しているようです。

また、このプログラムでは Google.Apis.CloutIot.v1 を使っているので、プロジェクトに追加します。

参照

Dockerイメージを使ってWebアプリをGoogle Compute Engineで動かす

ASP.NET Core MVCのアプリをGCPで公開できたので、その手順をまとめます。

前提

Container Registryの認証設定

Container RegistryにDockerイメージを登録する時にはdocker pushを使いますが、その前に認証設定が必要です。

認証方法 | Container Registry | Google Cloud

このドキュメントの書かれていることがよく分からず、とりあえず以下の2つを実行しました。

ただよく読むと、どちらかだけ行えばよかったのかもしれません。Docker 認証ヘルパーとしての gcloudの節には、

可能であれば、この方法を使用することを強くおすすめします。この方法では、プロジェクト リソースに安全で短期間のアクセス権が付与されます。

と書かれているので、どちらかであれば上の方法の方がよいみたいです。

とりあえず両方のやり方を載せます。

gcloudをDocker 認証ヘルパーとして使う設定

以下のコマンドを実行します。

$ gcloud auth configure-docker
WARNING: `docker-credential-gcloud` not in system PATH.
gcloud's Docker credential helper can be configured but it will not work until this is corrected.
WARNING: `docker` not in system PATH.
`docker` and `docker-credential-gcloud` need to be in the same PATH in order to work correctly together.
gcloud's Docker credential helper can be configured but it will not work until this is corrected.
The following settings will be added to your Docker config file
located at [C:\Users\loca\.docker\config.json]:
 {
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud"
  }
}

警告が出ていますが特に問題なくイメージの登録ができたので、今回は無視します。

スタンドアロン Docker 認証ヘルパー

以下の2つのコマンドを実行します。

$ gcloud components install docker-credential-gcr

Restarting command:
  $ gcloud components install docker-credential-gcr

$ docker-credential-gcr
C:\Users\loca\.docker\config.json configured to use this credential helper for GCR registries

DockerイメージをContainer Registryに登録する

以下のドキュメントに沿って登録します。

イメージの push と pull  |  Container Registry  |  Google Cloud

Dockerイメージへのタグ付け

Dockerイメージに以下のような決まったフォーマットのタグを付ける必要があるようです。

[HOSTNAME]/[PROJECT-ID]/[IMAGE]

HOSTNAMEはContainer Registryの場所で、以下の4つがあります。

・ gcr.io は米国でイメージをホストしていますが、今後は場所が変更される可能性があります。
・ us.gcr.io は米国でイメージをホストしますが、その場所は、gcr.io によってホストされるイメージからは独立したストレージ バケットです。
・ eu.gcr.io は、欧州連合でイメージをホストします。
・ asia.gcr.io は、アジアでイメージをホストします。

今回は無料枠のCompute Engineを使うので、それに合わせて米国に作られるようにus.gcr.ioを使います。※Compute Engineの無料枠は米国の特定のリージョンでなければ適用されないため。

PROEJCT-IDは今回のアプリを動かすプロジェクトのIDです。Googleアカウントでログインしていれば、プロジェクトのホームの「プロジェクト情報」に載っています。

https://console.cloud.google.com/home

IMAGEはDockerイメージの名前で、今回はsampleappです。

以上から、タグの名前は以下のようになります。

us.gcr.io/[PROJECT-ID]/sampleapp

そしたらdockerコマンドでイメージにタグを付けます。

$ docker tag sampleapp us.gcr.io/[PROJECT-ID]/sampleapp

docker pushでDockerイメージをContainer Registryに登録します。

$ docker push us.gcr.io/[PROJECT-ID]/sampleapp
The push refers to repository [us.gcr.io/[PROEJCT_ID]/sampleapp]
cd1a588c63a5: Pushed
e5f18c307014: Pushed
e7ba18774395: Pushed
a0afac1d9f1c: Pushed
3fb318ee8d39: Pushed
ef68f6734aa4: Layer already exists
latest: digest: sha256:c60eae957fe7a7729b4b3c3199d96985674641962f579d7010cd8edb43511ffb size: 1581

これでContainer Registoryにイメージが登録されます。

Compute EngineでDockerイメージを使ってアプリを起動する

Compute Engineのインスタンス作成画面で以下のように設定し、他の項目は適宜変更して作成します。

  • 「この VM インスタンスにコンテナ イメージをデプロイします。」にチェック
  • 「コンテナイメージ」にDockerイメージの名前を設定
    • us.gcr.io/[PROJECT-ID]/sampleapp
  • 「HTTP トラフィックを許可する」にチェック

f:id:LocaQ:20190114231752p:plain

作成が完了して起動したら、外部IPアドレスを確認してブラウザからアクセスします。

http://xxx.xxx.xxx.xxx/

これでとりあえずアプリの公開ができました。

もしアクセスできなければ、以下のページのトラブルシューティングが参考になるかもしれません。

Running a basic Apache web server  |  Compute Engine Documentation  |  Google Cloud

Flashforge Adventurer3 のノズル詰まりの直し方

Flashforge Adventurer3 でプリントを開始してもフィラメントがまったく出てこなくなってしまったので、ノズルにフィラメントが詰まっていると考えて除去したら直りました。

マニュアルに詰まりの解消方法が書いてありますが読んだだけでは細かいところがよく分からなかったので、また必要になった時のために実際にやった手順を書こうと思います。

マニュアルの確認

マニュアルの3章Q&AのQ1にノズルが詰まった時の解決方法が載っています。

f:id:LocaQ:20180825203526p:plain:w500

また、方法1の「エアーチューブジョイント」は、マニュアルの「1.1 部品紹介」の「2. フィラメントガイドチューブジョイント」の事だと思われます。

f:id:LocaQ:20180825203650p:plain:w600

ノズル詰まり解消の手順

マニュアルの方法1と2を行います。

フィラメントをアンロードする

手順ではアンロードしていないのでこれはやらなくてもいいと思いますが、自分はやってしまったので一応書いておきます。

アンロードは本来のメニューの「樹脂交換」を選択して行います。フィラメントがアンロードされたら「確認」を押さずに、「←」を押してメニューのトップに戻ります。

ヘッドを移動する

フィラメントの除去ではヘッドの上部からピンツールを押し込むので、ヘッドの場所によっては作業ができません。なのでヘッドを作業しやすい場所へ移動します。ヘッドの移動はメニューの「ツール」→「設定」→「移動」でできます。

移動先は最下部から少し上のあたりにしました。ヘッドの上部に拳を入れるので下の方、ただし下過ぎるとノズルから出たフィラメントを取りにくくなるので最下部より少し上、という感じです。

f:id:LocaQ:20180825210528j:plain:w800

エクストルーダーを加熱する

メニューの「ツール」→「加熱準備」を選択してエクストルーダーの温度を200℃まで加熱します。プラットフォームは加熱しなくていいのでOFFにします。

f:id:LocaQ:20180825211029j:plain:w800

ヘッドからガイドチューブを取る

「フィラメントガイドチューブジョイント」を押してチューブを取ります。チューブジョイントは画像の黒い部品で、これを下に押し込むと体感で1mmくらい下がります。その状態のままガイドチューブ(ジョイントに繋がっている白いやつ)を引っ張って抜きます。ちゃんとジョイントを押し込めていればそんなに力はいらなかったです。

※エクストルーダーが熱いので触らないように気を付けます。

f:id:LocaQ:20180825211844j:plain:w400

ガイドチューブを取れました。

f:id:LocaQ:20180825212344j:plain:w800

フィラメントを除去する

詰まり除去用ピンツールを使ってノズルの中のフィラメントを押し出して除去します。

詰まり除去用ピンツールは付属品のこんなやつです。

f:id:LocaQ:20180825212317j:plain:w400

ピンツールをチューブジョイントの部分に差し込みます。フィラメントが中に残っていると柔らかい感触がするので押し込みます。押し込むとノズルからフィラメントが出てきます。これを繰り返してフィラメントを全て押し出します

f:id:LocaQ:20180825212618j:plain:w800

なんとなくコツを書くと、

  • ゆっくり押し込む
  • 中の空洞の側面にあるフィラメントを下に押し込む感じでやる
  • ノズルからフィラメントが出てこなくなっても念を押して何度も繰り返す

という感じでやりました。

念を押して何度も繰り返すのは、もう出ないと思っても意外と出てくるためです。1回1回では変化がなくても何度もやると変化があったりします。そしてちゃんと除去できていないとおそらく次のフィラメントのロードでまた詰まってしまいます。私は2回やりました。

元に戻す

フィラメントを除去し終わったら、チューブジョイントを押し込みながらガイドチューブを差し込んで元の状態に戻します。

フィラメントをロードする

除去が終わったらメニューの「樹脂交換」→「押出」を選択してフィラメントをロードします。もしここでフィラメントが出てこず、フィラメント吸入口から「ガッガッ」といった感じの音がしたら、まだフィラメントが詰まっていると思いますので上の手順を再度やります。

フィラメントがノズルから出てくるようになったら成功です。

仕上げ

最後にメニューの「ツール」→「設定」→「校正」を選択して校正します。