Runner in the High

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

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

goコンパイラの最適化によって、可変長配列であってもリテラル値でlength指定されていればコンパイルする時点で確保するデータサイズが決定し結果的にランタイム時にアロケーションを発生させなくなるのではないかという仮説。

テストコード

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しか選択肢がないのであんまり悩むことがない。

Elmにおける比較処理の高速化テクニック

ここで触れられてた話がおもしろいかったのでざっくり紹介

discourse.elm-lang.org

文字列比較はcase文のほうが早い(ことがある)

これは遅い

ensureNonBreakingSpace : Char -> Char
ensureNonBreakingSpace char =
    if char == ' ' then
        nonBreakingSpace
    else
        char

これは速い

ensureNonBreakingSpace : Char -> Char
ensureNonBreakingSpace char =
    case char of
        ' ' ->
            nonBreakingSpace
        _ ->
            char

理由は以下の通り

Currently, a comparison with a character literal isn’t optimized in the same way an integer literal is. By using an elm case expression, we make the compiler generate a javascript case statement. These again are much faster because there is no function call involved.

今のElmコンパイラ(おそらく0.19)では文字リテラルの比較が最適化されていないため、こういう結果になる。

余談ではあるがElmコンパイラは比較の際にどちらかがリテラルではない場合にパフォーマンス劣化を起こす。なぜなら、ElmにおいてJSの等価演算子はオペレータで比較される2値のどちらかがリテラルでなければ生成されないからだ。

リテラル同士の比較は内部的にはJSの等価演算子だけではなくElmランタイムの比較処理を呼び出すため、その分のオーバーヘッドが発生することになる。したがって、数値の比較であっても x == y よりは (x - y) == 0 のほうがパフォーマンスには優れているとのこと。

これ以外にもcase文でMaybeやResultをパターンマッチするほうがパイプで関数をチェーンするよりも速い、レコード更新のシンタクス({ a | field = value } みたいなやつ)を使うよりも新しいレコードを生成するほうが速い、などの話が elm-physics の作者から出ているが、ベンチマークがあるわけではないので真偽は不明。