Runner in the High

技術のことをかくこころみ

makeのlengthとcapacityを間違えると予想外のアロケーションが発生することがある&bytes.Bufferの処理速度について

izumisy.work

上記の記事をstackoverflowで質問したら回答がついた。

stackoverflow.com

結論から言うと自分はmakeの使い方を間違えていて、makeの第3引数にあたるcapacityを省略してスライスを作成するとそこには空データが埋まってしまうとのこと。

なので、もともとの make([]byte, ALLOC_SIZE) だと、最初からALLOC_SIZE分のデータが詰まっている状態から始まるためappendする度に毎回拡張のアロケーションが発生していた。正しくは make([]byte, 0, ALLOC_SIZE) として宣言するべきで、こうすることでアロケーションの回数は bytes.Buffer を使ったときと同じになる。

この辺の話は以下の記事も詳しい。

note.com

bytes.Bufferとmakeの処理速度

bytes.Buffer も内部的にはmakeしているので、 理論上は bytes.Buffer よりもただ単純にmakeするだけのほうが速い。実際にそういうベンチマークの結果もある。

github.com

しかし、appendでデータを詰め始めたときの処理速度には違いが出る。

テストコードはこれ。

package app

import (
    "bytes"
    "testing"
)

const ALLOC_SIZE = 1024 * 1024 * 1024

func BenchmarkFunc1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := make([]byte, 0, ALLOC_SIZE)
        fill(v, '1', 0, ALLOC_SIZE)
    }
}

func BenchmarkFunc2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := new(bytes.Buffer)
        v.Grow(ALLOC_SIZE)
        fill(v.Bytes(), '2', 0, ALLOC_SIZE)
    }
}

func fill(slice []byte, val byte, start, end int) {
    for i := start; i < end; i++ {
        slice = append(slice, val)
    }
}

この結果は以下になる

at 13:32:12 ❯ go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: app
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFunc1-8               1        1489858750 ns/op        1073743744 B/op        4 allocs/op
BenchmarkFunc2-8               2         930927369 ns/op        1073742880 B/op        3 allocs/op
PASS
ok      app     4.395s

1.5倍近く処理速度の差がある。なお、Growしない場合にはbytes.Bufferのほうが遅い。

makeとbytes.Bufferでは少なくともアロケーションの回数は同等だがアロケーションの速度はmakeのほうが確実に速い。一方、appendしてデータを詰める処理をし始めるとmakeよりもbytes.BufferしてGrowしたほうが処理速度的に優位になるということが分かった。

ここまでの処理をまとめたベンチマークのテストの結果を整理すると以下になる。

処理 確保 確保 + 書き込み
make 2976935 ns/op 66047937 ns/op
bytes.Buffer 0.3202 ns/op 85604838 ns/op
bytes.Buffer (Grow) 2950193 ns/op 44761869 ns/op

bytes.BufferはGrowしなければ確保の速度は最速だが、データを書き込み始めるととんでもなく遅い。メモリだけ確保しておいてデータは書き込まないということは無いはずなので、あまり意味のない速度な気がする。

少なくともこの中で一番無難なのはbytes.BufferでGrow使うことなのかな。

リテラル値でmakeしたらヒープに乗らなくてアロケーションの回数は少なくなるんじゃないか?という実験

リテラルなんだからコンパイルする時点で確保するデータサイズが決定でき、結果ランタイム時にアロケーションが発生しなくなるのではないかという仮説。

テストコード

package app

import (
    "bytes"
    "testing"
)

const ALLOC_SIZE = 64 * 1024

func BenchmarkFunc1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        v := make([]byte, ALLOC_SIZE)
        fill(v, '1', 0, ALLOC_SIZE)
    }
}

func BenchmarkFunc2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := new(bytes.Buffer)
        b.Grow(ALLOC_SIZE)
        fill(b.Bytes(), '2', 0, ALLOC_SIZE)
    }
}

func fill(slice []byte, val byte, start, end int) {
    for i := start; i < end; i++ {
        slice = append(slice, val)
    }
}

これを実行した結果が以下。

at 19:05:47 ❯ go test -bench . -benchmem -gcflags=-m
# app [app.test]
./main_test.go:25:6: can inline fill
./main_test.go:10:6: can inline BenchmarkFunc1
./main_test.go:13:7: inlining call to fill
./main_test.go:20:9: inlining call to bytes.(*Buffer).Grow
./main_test.go:21:15: inlining call to bytes.(*Buffer).Bytes
./main_test.go:21:7: inlining call to fill
./main_test.go:10:21: b does not escape
./main_test.go:12:12: make([]byte, ALLOC_SIZE) escapes to heap
./main_test.go:20:9: BenchmarkFunc2 ignoring self-assignment in bytes.b.buf = bytes.b.buf[:bytes.m·3]
./main_test.go:17:21: b does not escape
./main_test.go:19:11: new(bytes.Buffer) does not escape
./main_test.go:25:11: slice does not escape
# app.test
/var/folders/45/vh6dxx396d590hxtz7_9_smmhqf0sq/T/go-build1328509211/b001/_testmain.go:35:6: can inline init.0
/var/folders/45/vh6dxx396d590hxtz7_9_smmhqf0sq/T/go-build1328509211/b001/_testmain.go:43:24: inlining call to testing.MainStart
/var/folders/45/vh6dxx396d590hxtz7_9_smmhqf0sq/T/go-build1328509211/b001/_testmain.go:43:42: testdeps.TestDeps{} escapes to heap
/var/folders/45/vh6dxx396d590hxtz7_9_smmhqf0sq/T/go-build1328509211/b001/_testmain.go:43:24: &testing.M{...} escapes to heap
goos: darwin
goarch: amd64
pkg: app
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkFunc1-8            8565            118348 ns/op          393217 B/op          4 allocs/op
BenchmarkFunc2-8           23332             53043 ns/op           65536 B/op          1 allocs/op
PASS
ok      app     2.902s

うーむ make([]byte, ALLOC_SIZE) escapes to heap と言われているのでヒープに乗ってしまった。仮説は間違ってたっぽい。

結局比較に使っている new(bytes.Buffer) のほうがアロケーションの回数も使うメモリの量も小さいと出た。go力が低すぎて理由が分からない。

(答え合わせの続編↓) izumisy.work

ioutil.Discardとio.CopyNでアロケーションコストを抑えてデータサイズを判定する

ioutil パッケージに Discard という /dev/null 的な io.Writer が用意されている。

最近これを使うタイミングがあったのでメモ。以下のようなコードがあるとする。

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "strings"
)

func main() {
    a := strings.NewReader("123456789")

    _, err := io.CopyN(ioutil.Discard, a, 10)
    if err == io.EOF {
        fmt.Println("more than 9")
    } else if err != nil {
        panic(err)
    }
}

上記のコードを実行すると more than 9 が出力になる。

ある io.Reader に対して io.CopyN でサイズ指定をしてデータコピーを試み、それがEOFかどうかを見ることで対象のデータのサイズが任意のサイズを超えているかどうかを疑似的に判定している、という感じ。データサイズの判定がしたいだけでコピー先のデータは捨ててしまってokなので ioutil.Discard を使っている。

このコードではイメージ付きづらいが、仮にsrc変数に300MiBとかのデータが入っているとすると、その具体的なサイズを取得するのに []byte などへ変換して len に通せばとんでもないアロケーションコストが発生することになる。一方で io.Reader のまま引き回せば []byte へ変換するアロケーションコストを抑えながらサイズ判定もできる。

net/mailパッケージだけでもメールアドレスのバリデーションはできる

stackoverflow.com

こんな感じで使える。

package main

import (
    "fmt"
    "net/mail"
)

func main() {
    addr, err := mail.ParseAddress("izumisy.test@example.com")
    if err != nil {
        panic(err)
    }

    fmt.Println(addr.Address) // izumisy.test@example.com
}

ひとつだけ気をつけないといけないところがあり、例えば <izumisy> izumisy.test@example.com (aaa) のようなメールアドレスもこのメソッドではエラー無しで通ってしまう。

RFC上は正しいのでバグではない&実際にはパースされてアドレス部だけが取り出せるので大きな問題にはならないが。

deferで参照している変数のポインタを更新しても参照は変わらないっぽい

こういうやつ

type Closer struct{
    Value string
}

func (c *Closer) Close(id string) {
    fmt.Printf("Closed(%s): %p\n", id, c)
}

func newCloser(c *Closer) {
    n := &Closer{Value: "aaa"}
    fmt.Printf("New: %p\n", n)
    c = n
}

func main() {
    c := &Closer{Value: "bbb"}
    fmt.Printf("Main: %p\n", c)
    defer c.Close("main") // <-- ここのcはnewCloserで作られたCloserのポインタになっているはず...?
    newCloser(c)
}

上記を実行するとこうなる

Main: 0xc000010240
New: 0xc000010250
Closed(main): 0xc000010240

newCloser関数の内部で参照渡しとしてリソースを上書きしたとしてもmainの中のdefer節で呼ばれるCloseメソッドの呼び出し対象の変数cの参照は古いままなので、newCloser関数の中で作られた方のリソースのクローズ漏れが起きる可能性がある。

これを防ぐためにはnewCloser関数のなかでもdeferでリソースのクローズをするしかない。

BlueoothドングルTP-Link UB400をUbuntuで使う

これを買った

バイス情報を見てみるとどうやら Cambridge Silicon Radio とかいうやつらしい。

at 15:26:52 ❯ lsusb
Bus 005 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 004 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 002 Device 003: ID 2357:011e TP-Link 802.11ac WLAN Adapter 
Bus 002 Device 005: ID 046d:c52b Logitech, Inc. Unifying Receiver
Bus 002 Device 004: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode)
Bus 002 Device 002: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

軽くググってみるとこのCSR製のやつはUbuntuと相性が悪いとか... beautifulajax.dip.jp

とりあえずBluetooth周りのデバイスは自分の使っている5.8.0-59-genericのカーネルにドングルがあれば動くには動くが、ちょっと気になるところもあり

あたりがちょいちょい不便で困っている。

バイスに原因があるのかLinux側に原因があるのかは分からないので、まだ試行錯誤中。

試したこと

なんとなくデバイスサスペンドが原因かもと思い、カーネルパラメータに usbcore.autosuspend=-1 btusb.enable_autosuspend=n をつけてみたりした。体感、急にBluetoothが切断されることは無くなったような気がする。

起動直後に繋がらないのを改善するために /etc/bluetooth/main.conf にあるFastConnectionとかいうやつもtrueにしてみたが、これもあんまりワークしてる気がしないな...

もし同じように困ってる人がいたら、このaskubuntuの回答がなんかヒントになるかも。

askubuntu.com

フロントエンドアプリケーションにおいて状態をどこに置くべきか論

後学のために自分の考えていることをまとめてみる。

考えられるパターン

これまでの経験から以下4つのパターンがある。

  1. ローカルStateでprop-drillingする
  2. ローカルStateかつイベント経由でデータ交換をする
  3. グローバルStoreとローカルStateを併用する
  4. グローバルStoreのみを使用する

1. ローカルStateでprop-drillingする

propとしてコンポーネント間のデータをやりとりする手法。

ほぼすべてのUIコンポーネントを親からデータを受け取りDOMを出力するだけの純粋な関数として表現できるため、全体の設計自体はシンプルになる。手間は多いが魔法は少ない。

コンポーネントの粒度が小さいアプリケーションの場合にはいわゆるバケツリレーと揶揄されるデータの受け渡しが頻発し、これに嫌悪感を持つエンジニアもいる。

2. ローカルStateかつイベント経由でデータ交換をする

Flux登場以前のフロントエンド・アプリケーションでよく見られた手法。

ほとんどFluxライブラリの再発明と同じであるが、コンポーネント間で双方向にデータが飛び交う点が大きな違い。イベントをインターフェイスとしてコンポーネントの関係性を疎結合にできる点がメリット。このデータの流れを単方向にしたものがFluxアーキテクチャである。

Vue.jsやAngular.jsにおいてはかつて親子間でのイベント送信を相互に行える機能が用意されていたため、これを使って横断的にコンポーネント間通信をする設計で開発が行われ徐々にカオスになっていく事例が見られた。いまはFluxパターンの登場によってアンチパターンになったと思われる。

3. グローバルStoreとローカルStateを併用する

仕様に応じてグローバルStoreとローカルStateを使い分ける。

一見良さそうなアイデアに見えるが、この手法の大きな問題点は「なにをStoreに置き、なにをStateに置くか」のルールづくりが大変なこと。チームが大きくなるとすぐにルールが守られなくなる。また、UIの改修によって状態の置き場がStateからStoreに移動したりなど、仕様変更時の影響範囲が状態管理部分を巻き込んで大きくなる可能性もある。

一方で、ローカルStateにすることで影響範囲を小さくしたり、別コンポーネントからの意図しないデータ参照を防ぐなどの設計も可能になるのがメリット。しかし、これはグローバルStoreにデータを置いたうえで仕様に応じた個別のデータ型を用意することでも達成できる。

4. グローバルStoreのみを使用する

グローバルStoreのみを状態管理に使う手法。

この手法では「グローバルStoreが膨らんで困る」とよく言われる。ココで言うところの「困る」とは大抵の場合「グローバルStoreのフィールドが増えて見づらくなる」と「グローバルStoreが大きくなると状態更新差分パフォーマンスが悪くなる」のふたつの懸念に集約される。これがクリアできるのであれば有用な手法になる。

利用するFluxライブラリによってはこれを解決できる機能や追加のプラグインなどが用意されていることがある。

どれを使うべきか。

まず、チームメンバが1人とか2人とかの場合にはどれを使おうが気にする必要はない場合が多い。好きなのを使えばよい。よっぽどめちゃくちゃな人がいなければコードベースは破綻しない。

しかし、経験上4-5人目くらいからが分岐点で、更に「2-3人のメンバで構成されるチームが複数ある」のようなケースになると途端に画一的なルール作りのほうが重要になってくる。設計方針に秩序を持たせたい場合には「いい感じでよしなにお願いします」がワークしなくなる。

ルールの作りやすさ順でいうと1>4>3>2かなという感じ。

Elmの場合には1しか選択肢がないのであんまり悩むことがない。