2019/08/06


Go の標準パッケージのコードには稀に意図的にそうなっているのか分からない、速度に寄与するのかどうか確かめたくなる物が入っている事があります。

先日も見つけました。まだマージされてないですが、os.Mkdir に NUL という文字列を渡した時にファーストパスでエラーを返す変更です。

186139: os: return an error when the argument of Mkdir on Windows is os.DevNull
https://go-review.googlesource.com/c/go/+/186139/5/src/os/file.go#563

ここで出てくる以下のコード。

func isDevNull(name stringbool 
    if len(name) != 3 {
        return false
    }
    if name[0]|0x20 != 'n' {
        return false
    }
    if name[1]|0x20 != 'u' {
        return false
    }
    if name[2]|0x20 != 'l' {
        return false
    }
    return true
}

3文字の NUL を大文字小文字無視で比較しています。ビットマスクで大文字小文字を同一視しつつ、1つでも条件にマッチしない物があれば即 false を返すという古き良きC言語的なハックが使われています。

さて、このコードは本当に速度に寄与するのでしょうか?

package lowercase

import (
    "strings"
    "testing"
)

func isDevNull1(name stringbool {
    if len(name) != 3 {
        return false
    }
    if name[0]|0x20 != 'n' {
        return false
    }
    if name[1]|0x20 != 'u' {
        return false
    }
    if name[2]|0x20 != 'l' {
        return false
    }
    return true
}

func isDevNull2(name stringbool {
    if len(name) != 3 {
        return false
    }
    if name[0!= 'n' && name[0!= 'N' {
        return false
    }
    if name[1!= 'u' && name[1!= 'U' {
        return false
    }
    if name[2!= 'l' && name[2!= 'L' {
        return false
    }
    return true
}

func isDevNull3(name stringbool {
    return strings.ToLower(name) == "nul"
}

var tests = []struct {
    in     string
    result bool
}{
    {"nul"true},
    {"Nul"true},
    {"nui"false},
    {"lun"false},
    {"nulllllllllllllll"false},
    {"nuuuuuuuul"false},
    {strings.Repeat("N"3000), false},
}

func test(b *testing.B, f func(string) bool) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        for _, test := range tests {
            if got := f(test.in); got != test.result {
                b.Fatalf("want %v but got %v for %v", test.result, got, test.in)
            }
        }
    }
}

func BenchmarkS1(b *testing.B) {
    test(b, isDevNull1)
}

func BenchmarkS2(b *testing.B) {
    test(b, isDevNull2)
}

func BenchmarkS3(b *testing.B) {
    test(b, isDevNull3)
}

isDevNull1 が今回のコード、isDevNull2 が改良前のコード、isDevNull3 が入力文字を予め小文字に変換し比較するコードです。ベンチマークの実行結果は以下の通り。

goos: windows
goarch: amd64
pkg: github.com/mattn/go-sandbox/b3
BenchmarkS1-4           47997120                25.5 ns/op
BenchmarkS2-4           41377027                28.5 ns/op
BenchmarkS3-4             136354              8786 ns/op
PASS
ok      github.com/mattn/go-sandbox/b3  3.840s

Windows 64bit Core i7 16GB の結果です。今回改良されるコードが微妙ながら速度に寄与している事が分かりました。逆に言えばこの程度しか寄与していないので、可読性を優先する様なコードであれば isDevNull2 で充分かなとも思います。

改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 Kindle版 / ¥2,350 (2019年08月01日)
 
発送可能時間:

Posted at by



2019/07/02


Go は Ducktype をサポートしたプログラミング言語です。interface で宣言されたメソッドを型に強制したり問い合わせたり出来ます。メソッドの実装を保証する為に以下の様に struct に interface を embedded する事も出来ます。

package main

type I interface {
    doSomething()
}

type foo struct {
    I
}

func (f *foo) doSomething() {
}

func callSomething(i I) {
    i.doSomething()
}

func main() {
    callSomething(&foo{})
}

ただしこうしてしまうと型 foo の情報にインタフェースの情報 I が含まれる事になり、struct のサイズが数バイト(アーキテクチャによります)大きくなってしまうのです。

package main

import (
    "fmt"
    "unsafe"
)

type I interface {
    doSomething()
}

type foo struct {
    I
}

func (f *foo) doSomething() {
}

type bar struct {
}

func (b *bar) doSomething() {
}

func main() {
    fmt.Printf("sizeof(%T) is: %v\n", foo{}, unsafe.Sizeof(foo{}))
    fmt.Printf("sizeof(%T) is: %v\n", bar{}, unsafe.Sizeof(bar{}))
}

https://play.golang.org/p/gpgX4teDhki

64bit の OS では16バイトです。配列になればもっと大きなサイズになってしまいますね。embedded をやめてしまってもコンパイル時に気付けるので問題ないのですが、ライブラリパッケージの場合はそれを使用するコードが無いのでテストを実施するまで気付けない事になります。なんだか時間が勿体ないですよね。そこで以下の様におまじないを入れておきます。

package zoo

var _ I = (*foo)(nil)

type I interface {
    doSomething()
}

type foo struct {
}

func (f *foo) doSomething() {
}

こうする事で、万が一 interface のメソッド宣言と struct のメソッドが不一致であっても即座に気付ける事になります。最近の Language Server を実装したテキストエディタであれば書いた瞬間にエラーになってくれるので、CI で実行するまで気付けないといった時間の無駄を削減する事が出来ます。

この方法は Go の FAQ にも書かれており、Go 本体のソースコードにも沢山書かれているテクニックです。

How can I guarantee my type satisfies an interface?

ぜひ無駄のない Go ライフを。

Posted at by



2019/06/30


自宅で動かしている物体認識サーバは TensorFlow を使って Go で書かれていたのだけど、CPU 負荷が高いので以前 go-tflite で書き換えた。その後 Raspberry Pi Zero W でそのまま使えるだろうと思っていたら結構リソースが厳しかったので C++ なウェブサーバの実装である crow と TensorFlow Lite を使って書き換える事にした。

GitHub - ipkn/crow: Crow is very fast and easy to use C++ micro web framework (inspired by Python Flask)

How to Build If you just want to use crow, copy amalgamate/crow_all.h and include it. Requirements C...

https://github.com/ipkn/crow/

ただし問題があって、crow は最新の boost を使ってビルドするとエラーが発生する。issue に書いたら親切な人がパッチを貼ってくれたのでローカルの amalgamate コードに適用した。

Cannot build on boost 1.70 · Issue #340 · ipkn/crow · GitHub

g++ -std=c++14 -c -I../include -I. helloworld.cpp -o helloworld.o In file included from ../include/c...

https://github.com/ipkn/crow/issues/340

これで実装できるだろうと思っていたら、なんと crow はマルチパートアップロードをサポートしていない事に気付く。他のウェブサーバ実装を探しても良かったのだけど、どうせ家で動かすウェブサーバだしアタックを受ける事もないので若干雑な実装ながらマルチパートアップロードを実装した。

typedef struct {
  std::map<std::string, std::string> header;
  std::string body;
} part;

static void
trim_string(std::string& s, const char* cutsel = \t\v\r\n") {
  auto left = s.find_first_not_of(cutsel);
  if (left != std::string::npos) {
    auto right = s.find_last_not_of(cutsel);
    s = s.substr(left, right - left + 1);
  }
}

static std::vector<std::string>
split_string(std::string& s, const std::string& sep, int n = -1) {
  std::vector<std::string> result;
  auto pos = std::string::size_type(0);
  while (n != 0) {
    auto next = s.find(sep, pos);
    if (next == std::string::npos) {
      result.push_back(s.substr(pos));
      break;
    }
    result.push_back(s.substr(pos, next - pos));
    pos = next + sep.length();
    --n;
  }
  return result;
}

static std::map<std::string, std::string>
parse_header(std::string& lines) {
  std::map<std::string, std::string> result;
  std::string::size_type pos;
  for (auto& line : split_string(lines, "\r\n")) {
    auto token = split_string(line, ":"2);
    if (token.size() != 2)
      break;
    trim_string(token[0]);
    std::transform(token[0].begin(), token[0].end(), token[0].begin(), ::tolower);
    trim_string(token[1]);
    result[token[0]] = token[1];
  }
  return result;
}

static std::vector<part>
parse_multipart(const crow::request& req, crow::response& res) {
  auto ct = req.get_header_value("content-type");
  auto pos = ct.find("boundary=");

  std::vector<part> result;
  if (pos != std::string::npos) {
    auto boundary = "\n--" + ct.substr(pos + 9);
    pos = boundary.find(";");
    if (pos != std::string::npos)
      boundary = boundary.substr(0, pos);
    pos = 0;
    auto& body = req.body;
    while (true) {
      auto next = body.find(boundary, pos);
      if (next == std::string::npos)
        break;
      auto data = body.substr(pos + boundary.size() + 2, next);
      auto eos = data.find("\r\n\r\n");
      if (eos == std::string::npos)
        break;
      auto lines = data.substr(0, eos);
      part p = {
        .header = parse_header(lines),
        .body = data = data.substr(eos + 4)
      };
      result.push_back(p);
      pos = next + boundary.size() + 1;
      if (body.at(pos) == '-' && body.at(pos + 1) == '-'
          && body.at(pos + 2) == '\n')
        break;
      else if (body.at(pos) != '\n')
        break;
    }
  }
  return result;
}

API 部分しか実装していないので使う場合は以下の様に curl コマンドを使う。

$ curl -X POST http://localhost:8888/upload -F "file=@grace_hopper.png;type=image/png"
[{"label":"bow tie","index":458,"probability":0.996078}]

OpenCV を使ってるので OpenCV がサポートしている画像フォーマットであれば使えるはず。適当なレベルではあるけど、こういうの欲しいと思ってる人もいるんじゃないかなと思ったので GitHub に置いておいた。

GitHub - mattn/crow-tflite-object-detect: Object detection API server using crow webserver and TensorFlow Lite.

crow-tflite-object-detect Object detection API server using crow webserver. Usage -1 curl -X POST htt...

https://github.com/mattn/crow-tflite-object-detect
Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術 Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術
渋川 よしき
オライリージャパン 単行本(ソフトカバー) / ¥3,199 (2017年06月14日)
 
発送可能時間:

Posted at by