[インデックス 19660] ファイルの概要
このコミットは、Go言語の標準ライブラリである archive/tar
パッケージ内の writer.go
ファイルに対する最適化です。具体的には、writeHeader
関数内で一時的に使用されるバッファの再利用を導入することで、メモリ割り当ての回数と量を削減し、パフォーマンスを向上させています。
コミット
commit fe5a358aaeb7e582b763125c1e05e601ccad3b63
Author: Cristian Staretu <unclejacksons@gmail.com>
Date: Thu Jul 3 09:40:53 2014 +1000
archive/tar: reuse temporary buffer in writeHeader
A temporary 512 bytes buffer is allocated for every call to
writeHeader. This buffer could be reused the lower the number
of memory allocations.
benchmark old ns/op new ns/op delta
BenchmarkWriteFiles100k 634622051 583810847 -8.01%
benchmark old allocs new allocs delta
BenchmarkWriteFiles100k 2701920 2602621 -3.68%
benchmark old bytes new bytes delta
BenchmarkWriteFiles100k 115383884 64349922 -44.23%
This change is very important if your code has to write a lot of
tarballs with a lot of files.
LGTM=dsymonds
R=golang-codereviews, dave, dsymonds
CC=golang-codereviews
https://golang.org/cl/107440043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fe5a358aaeb7e582b763125c1e05e601ccad3b63
元コミット内容
このコミットは、archive/tar
パッケージの writer.go
ファイルにおいて、writeHeader
関数が呼び出されるたびに一時的な512バイトのバッファが新たに割り当てられていた問題を解決します。このバッファを再利用することで、メモリ割り当ての回数を減らし、全体的なパフォーマンスを向上させることを目的としています。
ベンチマーク結果は以下の通りです。
- 実行時間 (ns/op): 約8.01%の改善 (634,622,051 ns/op から 583,810,847 ns/op へ)
- メモリ割り当て回数 (allocs): 約3.68%の削減 (2,701,920 回から 2,602,621 回へ)
- 割り当てられたバイト数 (bytes): 約44.23%の削減 (115,383,884 バイトから 64,349,922 バイトへ)
この変更は、多数のファイルを扱う多くのtarアーカイブを作成するようなコードにおいて、特に重要であると述べられています。
変更の背景
Go言語のランタイムは、ガベージコレクション(GC)によってメモリ管理を行っています。GCは不要になったメモリを自動的に解放する便利な機能ですが、頻繁なメモリ割り当てと解放はGCの負荷を増加させ、アプリケーションのパフォーマンスに悪影響を与える可能性があります。特に、短命なオブジェクトが大量に生成される場合、GCが頻繁に実行され、プログラムの実行が一時的に停止する「ストップ・ザ・ワールド」が発生しやすくなります。
archive/tar
パッケージの writeHeader
関数は、各ファイルエントリのヘッダを書き込む際に呼び出されます。この関数が呼び出されるたびに512バイトの新しいバッファが make([]byte, blockSize)
によって作成されていました。多数のファイルをtarアーカイブに書き込む場合、この writeHeader
関数が何度も呼び出され、結果として大量の短命な512バイトバッファが生成され、GCの負担となっていました。
このコミットの背景には、このような頻繁なメモリ割り当てを避け、既存のバッファを再利用することで、GCのオーバーヘッドを削減し、全体的なパフォーマンス、特にメモリ使用効率を改善するという目的があります。
前提知識の解説
Go言語のメモリ管理とガベージコレクション (GC)
Go言語は、自動メモリ管理のためにガベージコレクションを採用しています。開発者は手動でメモリを解放する必要がなく、ランタイムが不要になったメモリを自動的に回収します。しかし、GCは完全にオーバーヘッドがないわけではありません。
- メモリ割り当て (Allocation): プログラムが新しいオブジェクトを作成するたびに、メモリが割り当てられます。この割り当て自体にもコストがかかります。
- ガベージコレクション (GC): ランタイムは定期的にGCを実行し、到達不能なオブジェクト(もう参照されていないオブジェクト)を特定してメモリを解放します。GCの実行頻度や時間は、割り当てられるメモリの量やオブジェクトの寿命に影響されます。短命なオブジェクトが大量に生成されると、GCが頻繁にトリガーされ、アプリケーションの実行が一時的に停止する「ストップ・ザ・ワールド」が発生し、レイテンシが増加する可能性があります。
このコミットは、頻繁なメモリ割り当てを減らすことで、GCの負担を軽減し、アプリケーションのパフォーマンスを向上させる典型的な最適化手法です。
archive/tar
パッケージ
archive/tar
パッケージは、Go言語でTARアーカイブ(Tape Archive)を読み書きするための標準ライブラリです。TARファイルは、複数のファイルを一つのアーカイブにまとめるための一般的なフォーマットです。
Writer
構造体: TARアーカイブへの書き込みを管理する主要な構造体です。writeHeader
関数: TARアーカイブに個々のファイルやディレクトリのヘッダ情報(ファイル名、サイズ、パーミッションなど)を書き込む役割を担います。TARフォーマットでは、各ファイルエントリの前に512バイトのヘッダブロックが存在します。
blockSize
TARフォーマットでは、データは通常512バイトのブロック単位で扱われます。blockSize
はこの512バイトを指す定数です。ヘッダ情報もこの512バイトブロックに収まるように設計されています。
zeroBlock
zeroBlock
は、おそらく512バイトのゼロで埋められたバイトスライスまたは配列を指すものと考えられます。新しいヘッダバッファを初期化する際に、以前のデータが残らないようにゼロでクリアするために使用されます。
技術的詳細
このコミットの技術的な核心は、writeHeader
関数内で毎回新しいバッファを make
で作成する代わりに、Writer
構造体のフィールドとして固定サイズのバッファを保持し、それを再利用することです。
変更前は、writeHeader
が呼び出されるたびに以下の行で新しい512バイトのバイトスライスがヒープに割り当てられていました。
header := make([]byte, blockSize)
この header
スライスは writeHeader
関数のスコープ内でしか使用されず、関数が終了すると不要になり、GCの対象となります。多数のファイルを書き込む場合、この割り当てとGCが繰り返され、パフォーマンスのボトルネックとなっていました。
変更後は、Writer
構造体に hdrBuff [blockSize]byte
という固定サイズの配列が追加されました。
type Writer struct {
// ... 既存のフィールド ...
hdrBuff [blockSize]byte // buffer to use in writeHeader
}
[blockSize]byte
は、blockSize
(512)バイトの固定サイズの配列であり、これは Writer
構造体の一部としてスタックまたはヒープに割り当てられます(Writer
構造体自体がヒープに割り当てられる場合)。重要なのは、writeHeader
が呼び出されるたびに新しいメモリ割り当てが発生するのではなく、既存の hdrBuff
配列が再利用される点です。
writeHeader
関数内では、以下のように変更されました。
header := tw.hdrBuff[:]
copy(header, zeroBlock)
header := tw.hdrBuff[:]
:tw.hdrBuff
は配列ですが、[:]
を使うことで、その配列全体を指すスライスを作成しています。このスライスはhdrBuff
の基盤となる配列を参照するため、新しいメモリ割り当ては発生しません。copy(header, zeroBlock)
:zeroBlock
の内容(おそらく512バイトのゼロ)をheader
スライス(実体はtw.hdrBuff
配列)にコピーすることで、バッファを初期化し、以前のヘッダデータが残らないようにしています。
この変更により、writeHeader
が何回呼び出されても、hdrBuff
は一度だけ割り当てられ、その後は再利用されるため、ヒープ割り当ての回数が大幅に削減されます。これにより、GCの頻度が減り、全体的な実行時間とメモリ使用量が改善されます。ベンチマーク結果が示すように、特に割り当てられたバイト数の削減率(-44.23%)が顕著であり、この最適化がメモリ効率に大きく貢献していることがわかります。
コアとなるコードの変更箇所
src/pkg/archive/tar/writer.go
ファイルの変更点です。
--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -37,8 +37,9 @@ type Writer struct {
tnb int64 // number of unwritten bytes for current file entry
pad int64 // amount of padding to write after current file entry
closed bool
- usedBinary bool // whether the binary numeric field extension was used
- preferPax bool // use pax header instead of binary numeric header
+ usedBinary bool // whether the binary numeric field extension was used
+ preferPax bool // use pax header instead of binary numeric header
+ hdrBuff [blockSize]byte // buffer to use in writeHeader
}
// NewWriter creates a new Writer writing to w.
@@ -160,7 +161,8 @@ func (tw *Writer) writeHeader(hdr *Header, allowPax bool) error {
// subsecond time resolution, but for now let\'s just capture
// too long fields or non ascii characters
- header := make([]byte, blockSize)
+ header := tw.hdrBuff[:]
+ copy(header, zeroBlock)
s := slicer(header)
// keep a reference to the filename to allow to overwrite it later if we detect that we can use ustar longnames instead of pax
コアとなるコードの解説
-
Writer
構造体へのフィールド追加:Writer
構造体にhdrBuff [blockSize]byte
という新しいフィールドが追加されました。hdrBuff
: ヘッダデータを格納するためのバッファです。[blockSize]byte
:blockSize
はTARヘッダの標準サイズである512バイトを指す定数です。これにより、hdrBuff
は512バイトの固定サイズのバイト配列として定義されます。この配列はWriter
構造体のインスタンスが作成される際に一度だけ割り当てられ、その後のwriteHeader
の呼び出しで再利用されます。
-
writeHeader
関数内のバッファ作成ロジックの変更:writeHeader
関数内で、ヘッダバッファを作成する部分が変更されました。- 変更前:
header := make([]byte, blockSize)
この行は、writeHeader
が呼び出されるたびに、ヒープ上に新しい512バイトのバイトスライスを割り当てていました。これは頻繁なGCの原因となっていました。 - 変更後:
header := tw.hdrBuff[:] copy(header, zeroBlock)
header := tw.hdrBuff[:]
:tw.hdrBuff
はWriter
構造体のフィールドとして既に存在する配列です。[:]
を使用することで、この配列全体を参照するスライスheader
を作成します。この操作は新しいメモリ割り当てを伴いません。header
スライスはtw.hdrBuff
配列の基盤となるメモリを共有します。copy(header, zeroBlock)
:zeroBlock
はおそらく512バイトのゼロで埋められたバイトスライスです。このcopy
関数は、header
スライス(つまりtw.hdrBuff
配列)の内容をゼロで上書きし、以前のヘッダデータが残らないようにバッファをクリアします。これにより、常にクリーンな状態でヘッダデータを書き込むことができます。
- 変更前:
この変更により、writeHeader
が何回呼び出されても、hdrBuff
配列は一度だけ割り当てられ、その後は再利用されるため、メモリ割り当ての回数と量が劇的に削減され、結果としてパフォーマンスが向上します。
関連リンク
- Go言語の
archive/tar
パッケージのドキュメント: https://pkg.go.dev/archive/tar - Go言語のガベージコレクションに関する情報 (一般的な概念): https://go.dev/doc/gc-guide
参考にした情報源リンク
- コミットメッセージとベンチマーク結果
- Go言語の
archive/tar
パッケージのソースコード - Go言語のメモリ管理とガベージコレクションに関する一般的な知識
- Go言語の
make
関数とスライスの挙動に関する知識 - Go言語の
copy
関数に関する知識 - TARファイルフォーマットの基本