Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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

コアとなるコードの解説

  1. Writer 構造体へのフィールド追加: Writer 構造体に hdrBuff [blockSize]byte という新しいフィールドが追加されました。

    • hdrBuff: ヘッダデータを格納するためのバッファです。
    • [blockSize]byte: blockSize はTARヘッダの標準サイズである512バイトを指す定数です。これにより、hdrBuff は512バイトの固定サイズのバイト配列として定義されます。この配列は Writer 構造体のインスタンスが作成される際に一度だけ割り当てられ、その後の writeHeader の呼び出しで再利用されます。
  2. writeHeader 関数内のバッファ作成ロジックの変更: writeHeader 関数内で、ヘッダバッファを作成する部分が変更されました。

    • 変更前: header := make([]byte, blockSize) この行は、writeHeader が呼び出されるたびに、ヒープ上に新しい512バイトのバイトスライスを割り当てていました。これは頻繁なGCの原因となっていました。
    • 変更後:
      header := tw.hdrBuff[:]
      copy(header, zeroBlock)
      
      • header := tw.hdrBuff[:]: tw.hdrBuffWriter 構造体のフィールドとして既に存在する配列です。[:] を使用することで、この配列全体を参照するスライス header を作成します。この操作は新しいメモリ割り当てを伴いません。header スライスは tw.hdrBuff 配列の基盤となるメモリを共有します。
      • copy(header, zeroBlock): zeroBlock はおそらく512バイトのゼロで埋められたバイトスライスです。この copy 関数は、header スライス(つまり tw.hdrBuff 配列)の内容をゼロで上書きし、以前のヘッダデータが残らないようにバッファをクリアします。これにより、常にクリーンな状態でヘッダデータを書き込むことができます。

この変更により、writeHeader が何回呼び出されても、hdrBuff 配列は一度だけ割り当てられ、その後は再利用されるため、メモリ割り当ての回数と量が劇的に削減され、結果としてパフォーマンスが向上します。

関連リンク

参考にした情報源リンク

  • コミットメッセージとベンチマーク結果
  • Go言語の archive/tar パッケージのソースコード
  • Go言語のメモリ管理とガベージコレクションに関する一般的な知識
  • Go言語の make 関数とスライスの挙動に関する知識
  • Go言語の copy 関数に関する知識
  • TARファイルフォーマットの基本