なぜioutil.ReadFileはioutil.ReadAllより速いか

2 min read

TL;DR

Goでファイル内容を読む場合 には,ioutil.ReadFile の方が ioutil.ReadAll よりも高速.なぜなら,読み込むデータの大きさがあらかじめわかっている場合は,内部のバッファサイズを決定でき,無駄なメモリ確保を無くせるから.

(いやなんでReadAllを使うんだよ,というのはさておき.)

ioutilパッケージの関数たち

Go言語には入力や出力を抽象化したインターフェース(io.Readerio.Writer など)がある.このインターフェースはいわゆるファイル的な振る舞いをするものをまるっと同じように扱うためにとても便利なもの.ioutil パッケージも当然,それらをベースとしてさまざまな関数を実装している.

ただし,抽象化するということは,それぞれに特化できないということでもある.実際に ioutil.ReadAll のコードを読むと,最初に512 バイトのバッファを用意し,ファイルのEOFを検知するまで2倍,4倍,8倍…とそのサイズを大きくしながら読み込みを行っている.これは,io.Reader から一体どのくらいのデータを読み込むかわからないために行うバッファリングの処理である.

そこで,ioutil.ReadFile関数では,事前にosパッケージを使ってファイルの大きさを取得し,バッファサイズをそのとおりに確保することで一度にすべての内容を読み込んでいる.ioutil.ReadAll と同じAPIを使いたい場合には,ファイルオープンしてサイズを取得したあとに,io.ReadFullio.ReadAtLeastを使うと良いと思う.

ベンチマーク

  • ソースコード

    最初の関数は固定長のバッファで読み込んだ場合.次は ioutil.ReadAll を使う場合.これは指数的にバッファサイズを大きくしていくので可変長のバッファで読み込むということ.次に iotuil.ReadFile.最後がioutil.ReadFileと同等の処理をファイルサイズ取得+io.ReadAllで実装したもの.

package main

import (
	"io"
	"io/ioutil"
	"os"
	"testing"
)

var filename = "bigfile" // 804,335,663 bytes

func BenchmarkFixedSizeBuffer(b *testing.B) {
	BUFSIZE := 4 * 1024

	for i := 0; i < b.N; i++ {
		file, err := os.Open(filename)
		if err != nil {
			panic(err)
		}
		defer file.Close()

		data := make([]byte, 0, BUFSIZE)

		buf := make([]byte, BUFSIZE)
		for {
			n, err := file.Read(buf)
			if n == 0 {
				break
			}
			if err != nil {
				panic(err)
			}
			data = append(data, buf...)
		}
	}
}

func BenchmarkReadAll(b *testing.B) {
	for i := 0; i < b.N; i++ {
		file, err := os.Open(filename)
		if err != nil {
			panic(err)
		}
		defer file.Close()

		_, err = ioutil.ReadAll(file)
		if err != nil {
			panic(err)
		}
	}
}

func BenchmarkReadFile(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, err := ioutil.ReadFile(filename)
		if err != nil {
			panic(err)
		}
	}
}

func BenchmarkReadFull(b *testing.B) {
	for i := 0; i < b.N; i++ {
		file, err := os.Open(filename)
		if err != nil {
			panic(err)
		}
		defer file.Close()

		fi, err := file.Stat()
		if err != nil {
			panic(err)
		}

		data := make([]byte, fi.Size())
		_, err = io.ReadFull(file, data)
		if err != nil {
			panic(err)
		}
	}
}
  • 結果
❯ go test -bench . -benchmem -o pprof/test.bin  -cpuprofile pprof/cpu.out
goos: darwin
goarch: amd64
pkg: github.com/raahii/go-sandbox/read-bigfile
BenchmarkFixedSizeBuffer-4             3         363437901 ns/op        1293321805 B/op       51 allocs/op
BenchmarkReadAll-4                     7         171374293 ns/op        536869438 B/op        23 allocs/op
BenchmarkReadFile-4                   21          47905628 ns/op        209305932 B/op         5 allocs/op
BenchmarkReadFull-4                   25          44724078 ns/op        209305966 B/op         5 allocs/op
PASS
ok      github.com/raahii/go-sandbox/read-bigfile       7.531s

💁‍♀️ 💬

ただ,あくまでI/Oが高速であるという前提があるから成り立つ話であり,ネットワーク越しのデータアクセスのようにI/Oが遅い場合には.うまく並行処理を使ってバッファリングすると高速になるケースがあると思う.特に,最近インターンでAWS S3の大きめのファイルにアクセスする時にパフォーマンスが落ちる問題があったのでbufioまわりのパッケージを読んでみようと思う.

comments powered by Disqus

こちらの記事もどうぞ