最近、大学のサークルの人たちとGoを使ってのDB自作を始めました。
参考にしているのはこちらの本です:
この本は、実際に簡単なDBの実装を追いながらデータベースの実装や中身の仕組みについて学んでいくという内容になっており、Javaによるサンプルコードも公開されています。
これを参考にしながらGoで実装していこうという感じです。
疑問
ストレージ周りの操作に関する部分にて、参考にしている本の実装が、Open
したファイルをClose
せずに使い回すというものになっていました。
ここで一つ疑問が湧きました。os.File
は、Openしたとてそのファイルの内容がメモリに読み込まれるわけではありません。実際に読むためにはRead
が必要なはずです。
ということはつまり、os.File
はファイルへの参照を持っているだけであり、ファイルアクセスに伴うオーバーヘッドはOpen/Close操作ではほぼ発生しないはずです。
だというのに、本の提供する実装ではos.File
に対応するオブジェクトをClose
せずに使いまわしています。
本当にこのコードに意味はあるのでしょうか?
仮説と検証
意味のない実装が本に載るわけがありません。
きっとos.Open
はそれなりに重い操作で、それを回避するためにこういった実装になっているのでしょう。
というわけで、ファイルを読むたびにos.Open
とos.Close
をするコード、os.Open
したまま繰り返しファイルを読むコードを動かして比較してみることにしました。
検証に使ったのは以下のコードです:
package main
import (
"fmt"
"os"
"time"
)
func main() {
iteration:=100000
buf := make([]byte, 1)
start := time.Now()
for i:=0; i<iteration; i++ {
f, _ := os.Open("test.txt")
f.Read(buf)
f.Close()
}
fmt.Printf("open/close, for %v times: %vms\n", iteration, time.Since(start).Milliseconds())
start = time.Now()
f, _ := os.Open("test.txt")
for i:=0; i<iteration; i++ {
f.Seek(0,0) // 毎回ファイルの頭から読み始めるため
f.Read(buf)
}
f.Close()
fmt.Printf("keep opened, for %v times: %vms\n", iteration, time.Since(start).Milliseconds())
}
これを手元のUbuntuで動かしてみると、結果として以下のような出力が得られました:
$ go run main.go
open/close, for 100000 times: 436ms
keep opened, for 100000 times: 73ms
というわけで、10万回ファイルを読む場合について、その都度ファイルをOpenしてCloseする場合には436ms、一度Openしたファイルをそのまま使用する場合には73msと、およそ6倍ほど速くなることが判明しました。
それなりにos.Openは重いようです。
備考
Unixでのgoの実装では、File
はファイルディスクリプタを保持する実装になるようです。
type File struct {
*file // os specific
}
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
pfd poll.FD
name string
dirinfo atomic.Pointer[dirInfo] // nil unless directory being read
nonblock bool // whether we set nonblocking mode
stdoutOrErr bool // whether this is stdout or stderr
appendMode bool // whether file is opened for appending
}
// FD is a file descriptor. The net and os packages use this type as a
// field of a larger type representing a network connection or OS file.
type FD struct {
...
ということは、今回の結果を加味すると、FDの取得にある程度のオーバーヘッドがあるという感じなんですかね。