Goの勉強 低レベルプログラミング

Goの勉強をやり始めたのでメモ。
プログラミング言語 Go」を読んでる。

  • Go言語は安全性についての特性を多く持っている。
    • 文字列の減算はできない。
    • スライスなどの組み込み型の内部への直接的なアクセスを防ぐ。
    • 関数の機械語のコードにアクセスできない。
    • ゴルーチンがどのOSスレッドで実行されているか知ることができない。
    • 変数の実際のアドレスが変わっても、ポインタで同じようにアクセスできる。
  • 背後にある詳細を隠蔽することで、Goのプログラムは移植性が高い。
  • ただし、最高の性能を達成したいなどの理由により、これらの安全性を犠牲にしたい場合がある。
  • unsafe パッケージは通常のプログラムではほぼ使われない。

unsafe.Sizeof、Alignof, Offsetof

  • unsafe.Sizeof はバイト数の大きさを返す。
    • 例えば float64 は 8 バイト、文字列は 16 バイト(2ワード)
  • コンピュータはこれらの値が適切に整列されているときに最も効率的にメモリへ読み書きできる。
    • つまり、 float64 のアドレスは8の倍数であるべき。
    • 合成型の値の大きさは、があるかもしれないので、フィールドの大きさの合算よりも大きくなる可能性がある。
fmt.Println(unsafe.Sizeof(struct {  
    bool  
    float64  
    int16  
}{})) // 24(3ワード)  

fmt.Println(unsafe.Sizeof(struct {  
    float64  
    int16  
    bool  
}{})) // 16(2ワード)  

fmt.Println(unsafe.Sizeof(struct {  
    bool  
    int16  
    float64  
}{})) // 16(2ワード)  
  • unsafe.Alignof は必要な整列を返す。
    • おそらく、つまり、先頭のアドレスが何バイトの倍数であるべきか。基本的に1ワードよりも大きく整列されることはないので、8バイトより大きくなることはない?
  • unsafe.Offsetof はフィールドに対して構造体の先頭からのオフセットを穴も考慮して返す。
  • これらの関数は名前にも関わらず安全である。

unsafe.Pointer

  • unsafe.Pointer 型は、いかなる変数のアドレスでも保持できる特殊なポインタである。
    • 通常のポインタから unsafe.Pointer を通して値を参照することはできない。(型が不明なため)
    • 比較可能であり、ゼロ値は nil である。
  • 普通の *Tunsafe.Pointer に変換することができる。
  • unsafe.Pointer*T に変換することもできる。
    • 結果として任意の値をメモリに書き込むことができるため、型システムを破壊する。
  • unsafe.Pointeruintptr に変換することができる。
  • uintptrunsafe.Pointer に変換することもできる。
    • 同様に型システムを破壊する。
  • 多くの unsafe.Pointer の値は、普通のポインタの実際の数値的なアドレスへの変換とその逆変換に関する仲介を果たす。
  • 以下のコードは引越しさせるGCの考慮が抜けているため、正しく動作しない可能性がある。
    • 2行目に到達するタイミングで変数 x のアドレスは変わっている可能性があるため。
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)  
pb := (*int16)(unsafe.Pointer(tmp))  
*pb = 42  
  • 以下のコードは誤りである。
    • new で生成した変数はどこからも参照されておらず、すぐにガベージコレクタにより回収されるため。
pT := uintptr(unsafe.Pointer(new(T)))  
  • よって、 uintptrの値はすべて「以前のアドレスを含んでいる」とし、 unsafe.Pointer から uintptr への変換や uintptr を使う操作の回数を最低限にするべきである。
    • 可能であれば、1つの式の中に含めたほうが良い。
  • reflect パッケージなどの uintptr を返すメソッドを使用する場合は、なるべくすぐに unsafe.Pointer 型に変換するべきである。

cgo を使った C のコードの呼び出し

  • 多くのパッケージが、その実装の言語に関係なく C と互換性がある API を公開している。
    • go では cgo がそれにあたる。(が、唯一ではない)
    • このようなツールを 外部関数インターフェース(FFI) という。
    • cgoを使うのは、C の API が複雑であり、かつ性能が重要である場合である。
      • API が単純であれば、Go に移植している。
      • 性能が重要でなければ、 os/exec を使って呼び出せば良い。
  • cgoは import "C" の前に書いたコメントにより、コンパイラのオプションを指定できる。
    • #include など。
    • #cgo を使うことで、Cのツールチェーンに対する追加のオプションを指定できる。
  • C.bz_stream などのように C. を付けることで、Cの API を使用できる。
  • C.uintuint は同じ幅だが、型としては区別される。
  • Cに渡すポインタは基本的に unsafe.Pointer を使用する。
  • CのライブラリをGoに組み込むだけでなく、GoのライブラリをCに組み込むことも可能。

もう一つの注意書き

  • 高級言語は隔離層があることにより、安全で頑強、そして移植性の高いプログラムを書ける。
  • unsafe はその隔離層を通り越すため、プログラマは自分の責任で unsafe を使用する。
  • unsafe を使用すると、主に移植性と安全性でコストやリスクが発生する。
  • 可能な限り reflectunsafe の使用は避けたほうが良い。