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

[インデックス 12446] ファイルの概要

このコミットは、Go言語の標準ライブラリであるfmtパッケージとlogパッケージから、unicodeパッケージおよびbytes.Bufferへの依存を排除し、より軽量で効率的な実装に置き換えることを目的としています。これにより、これらのコアパッケージのフットプリントが削減され、Goアプリケーション全体のパフォーマンスとバイナリサイズに貢献します。

コミット

commit 0bc18811b54b782a56b171eb0f8974f47f8638c0
Author: Russ Cox <rsc@golang.org>
Date:   Tue Mar 6 23:27:11 2012 -0500

    fmt, log: stop using unicode
    
    $ go list -f '{{.ImportPath}} {{.Deps}}' fmt log
    fmt [errors io math os reflect runtime strconv sync sync/atomic syscall time unicode/utf8 unsafe]
    log [errors fmt io math os reflect runtime strconv sync sync/atomic syscall time unicode/utf8 unsafe]
    
    R=bradfitz, rogpeppe, r, r, rsc
    CC=golang-dev
    https://golang.org/cl/5753055
---
 src/pkg/fmt/export_test.go    |   7 ++\
 src/pkg/fmt/fmt_test.go       |  11 ++\
 src/pkg/fmt/format.go         |  19 ++-\
 src/pkg/fmt/print.go          |  58 +++++++--
 src/pkg/fmt/scan.go           | 113 +++++++++++++-----\
 src/pkg/go/build/deps_test.go | 267 ++++++++++++++++++++++--------------------\
 src/pkg/log/log.go            |  54 ++++-----\
 7 files changed, 313 insertions(+), 216 deletions(-)

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0bc18811b54b782a56b171eb0f8974f47f8638c0

元コミット内容

このコミットの元のメッセージは以下の通りです。

fmt, log: stop using unicode

$ go list -f '{{.ImportPath}} {{.Deps}}' fmt log
fmt [errors io math os reflect runtime strconv sync sync/atomic syscall time unicode/utf8 unsafe]
log [errors fmt io math os reflect runtime strconv sync sync/atomic syscall time unicode/utf8 unsafe]

R=bradfitz, rogpeppe, r, r, rsc
CC=golang-dev
https://golang.org/cl/5753055

このメッセージは、fmtlogパッケージがunicodeおよびunicode/utf8に依存している現状を示し、その依存を停止するという意図を明確にしています。

変更の背景

Go言語の標準ライブラリは、その効率性と軽量性を重視して設計されています。特に、fmt(フォーマット済みI/O)やlog(ロギング)のようなコアパッケージは、多くのGoプログラムで利用されるため、その依存関係を最小限に抑えることが重要です。

unicodeパッケージは、広範なUnicode文字プロパティや操作を提供しますが、そのデータテーブルは比較的大規模になる可能性があります。fmtlogのようなパッケージがunicodeパッケージの機能のごく一部(例えば、空白文字の判定など)しか必要としない場合、パッケージ全体をインポートすることは、不必要なバイナリサイズの増加やコンパイル時間の延長につながります。

同様に、bytes.Bufferはバイトシーケンスを効率的に構築するための便利なツールですが、内部的なオーバーヘッドや汎用性ゆえに、特定の高性能なシナリオでは直接[]byteスライスを操作する方がより効率的である場合があります。

このコミットは、これらの背景から、fmtlogパッケージの依存関係を削減し、パフォーマンスを最適化するための具体的なステップとして実施されました。

前提知識の解説

このコミットの変更内容を理解するためには、以下の知識が役立ちます。

  • Go言語のパッケージシステム: Goでは、コードはパッケージに分割され、import文を使用して他のパッケージの機能を利用します。パッケージ間の依存関係は、コンパイルされるバイナリのサイズやコンパイル時間に影響を与えます。
  • rune型とUnicode: Goのrune型はUnicodeコードポイントを表します。unicodeパッケージは、runeのプロパティ(例: unicode.IsSpaceで空白文字か判定)や操作を提供します。unicode/utf8パッケージは、UTF-8エンコーディングされたバイトスライスとrune間の変換を扱います。
  • bytes.Buffer: bytes.Bufferは、可変長のバイトバッファを提供するGoの標準ライブラリ型です。文字列やバイトスライスを効率的に構築する際によく使用されます。内部的には[]byteスライスを保持し、必要に応じて容量を拡張します。
  • []byteスライスとappend関数: Goの[]byteはバイトの動的配列(スライス)です。append関数は、スライスに要素を追加するために使用され、必要に応じて新しい基底配列を割り当ててデータをコピーします。直接[]byteを操作することは、bytes.Bufferを介するよりも低レベルで、特定の状況ではより効率的です。
  • Go標準ライブラリの設計原則: Goの標準ライブラリは、シンプルさ、効率性、そして依存関係の最小化を重視しています。特にコアパッケージでは、パフォーマンスとフットプリントの最適化が継続的に行われます。
  • Goのビルドシステムと依存関係の階層(deps_test.go): src/pkg/go/build/deps_test.goファイルは、Go標準ライブラリ内のパッケージ間の依存関係の「階層」を定義しています。L0が最も基本的なパッケージ群を表し、L1L2と続くにつれて、より高レベルな機能を持つパッケージが定義されます。この階層は、Goのビルドプロセスにおける依存関係の健全性を検証するために使用されます。

技術的詳細

このコミットは、fmtlogパッケージの内部実装を大幅に変更し、外部依存を減らすことで、より自己完結型で効率的な動作を実現しています。

  1. unicodeパッケージの依存性排除:

    • fmt/scan.goでは、以前unicode.IsSpaceを使用して空白文字を判定していましたが、このコミットではunicodeパッケージのインポートを完全に削除しました。代わりに、isSpaceという独自の関数が実装され、Goのunicodeパッケージが内部で利用しているWhite_SpaceのUnicode範囲を直接spaceという[][2]uint16型の定数スライスとして定義し、その範囲に基づいてルーンが空白文字であるかを判定します。これにより、fmtパッケージはunicodeパッケージ全体をリンクする必要がなくなり、バイナリサイズが削減されます。
    • fmt/format.goでは、文字が印刷可能であるかを判定するためにunicode.IsPrintの代わりにstrconv.IsPrintを使用するように変更されました。strconvパッケージは数値変換のために既にfmtの依存パッケージであるため、新たな依存を追加することなく同等の機能を実現しています。
    • fmt/print.goでは、unicode.MaxRuneutf8.MaxRuneに置き換えられています。これは、fmtパッケージがUnicodeの全範囲ではなく、UTF-8で表現可能な最大ルーン値(U+10FFFF)のみを考慮すれば十分であることを示しています。
  2. bytes.Bufferから[]byteへの移行とカスタムバッファの実装:

    • fmt/print.goでは、bytes.Bufferの代わりにbufferという新しいカスタム型が導入されました。このbuffer型は実質的に[]byteスライスのエイリアスであり、WriteWriteStringWriteByteWriteRuneといったbytes.Bufferと同様のメソッドが実装されています。これらのメソッドは、内部の[]byteスライスに対して直接append関数を使用してデータを追加します。これにより、bytes.Bufferが持つ汎用的な機能のオーバーヘッドを避け、fmtパッケージが必要とする最小限のバッファ操作を直接[]byteスライスに対して行うことで、パフォーマンスの向上とメモリ使用量の最適化を図っています。
    • bytes.BufferReset()メソッドは、スライスの長さをゼロにするp.buf = p.buf[:0]に置き換えられています。これは、既存のメモリを再利用しつつバッファをクリアする効率的な方法です。
    • bytes.BufferWriteToメソッドの呼び出しも、w.Write(p.buf)のように直接io.Writer[]byteを書き込む形に変更されています。
    • log/log.goでも同様に、Logger構造体のbufフィールドがbytes.Bufferから直接[]byteに変更されています。itoaformatHeaderといった内部関数は、*[]byteポインタを受け取るように変更され、append関数を直接使用してバイトスライスにデータを追加するように書き換えられています。これにより、logパッケージもbytes.Bufferへの依存を解消し、より効率的なログ出力が可能になります。
  3. 依存関係グラフの更新(src/pkg/go/build/deps_test.go):

    • このファイルは、Go標準ライブラリのパッケージ間の依存関係を定義しており、ビルドシステムが依存関係の健全性を検証するために使用されます。
    • fmtlogパッケージがunicodeへの依存を解消したことに伴い、このファイル内の依存関係定義も更新されました。特に、unicodeパッケージはL1からL2レベルに移動し、fmtlogL1(およびその他の特定のパッケージ)に依存するように変更されました。これは、fmtlogがより基本的な依存関係の階層に位置づけられることを意味し、Goのコアライブラリの軽量化戦略を反映しています。

これらの変更は、Goの標準ライブラリが、パフォーマンスと依存関係の最小化を重視していることを明確に示しています。特に、コアとなるfmtlogのようなパッケージでは、わずかな依存関係の削減や最適化が、Goアプリケーション全体のパフォーマンスとバイナリサイズに大きな影響を与える可能性があります。

コアとなるコードの変更箇所

src/pkg/fmt/print.goにおけるbytes.Bufferからカスタムbuffer型への変更

// 変更前 (概念)
// type pp struct {
//     // ...
//     buf       bytes.Buffer // bytes.Bufferを使用
//     // ...
// }
//
// func (p *pp) free() {
//     if cap(p.buf.Bytes()) > 1024 { // bytes.BufferのBytes()メソッド
//         return
//     }
//     p.buf.Reset() // bytes.BufferのReset()メソッド
//     // ...
// }
//
// func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
//     // ...
//     n64, err := p.buf.WriteTo(w) // bytes.BufferのWriteToメソッド
//     // ...
// }

// 変更後
type buffer []byte // 新しいカスタム型 ([]byteのエイリアス)

// buffer型にWrite, WriteString, WriteByte, WriteRuneメソッドを実装
func (b *buffer) Write(p []byte) (n int, err error) {
	*b = append(*b, p...)
	return len(p), nil
}
// ... (他のメソッドも同様に実装)

type pp struct {
	// ...
	buf       buffer // bytes.Bufferからカスタムbuffer型に変更
	// ...
}

func (p *pp) free() {
	// Don't hold on to pp structs with large buffers.
	if cap(p.buf) > 1024 { // カスタムbuffer型 ([]byte) のcap()を使用
		return
	}
	p.buf = p.buf[:0] // []byteスライスの長さをゼロにする (Reset()の代替)
	// ...
}

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n64, err := w.Write(p.buf) // io.Writerに直接[]byteを書き込む
	p.free()
	return int(n64), err
}

src/pkg/fmt/scan.goにおけるunicode.IsSpaceのインライン化

// 変更前 (概念)
// import (
//     // ...
//     "unicode" // unicodeパッケージをインポート
//     // ...
// )
//
// func notSpace(r rune) bool {
//     return !unicode.IsSpace(r) // unicode.IsSpaceを使用
// }

// 変更後
// import (
//     // ...
//     // "unicode" のインポートを削除
//     // ...
// )

// space is a copy of the unicode.White_Space ranges,
// to avoid depending on package unicode.
// unicode.White_Spaceの範囲を直接定義
var space = [][2]uint16{
	{0x0009, 0x000d},
	{0x0020, 0x0020},
	{0x0085, 0x0085},
	{0x00a0, 0x00a0},
	{0x1680, 0x1680},
	{0x180e, 0x180e},
	{0x2000, 0x200a},
	{0x2028, 0x2029},
	{0x202f, 0x202f},
	{0x3000, 0x3000},
}

// isSpace関数を独自に実装
func isSpace(r rune) bool {
	if r >= 1<<16 {
		return false
	}
	rx := uint16(r)
	for _, rng := range space {
		if rx < rng[0] {
			return false
		}
		if rx <= rng[1] {
			return true
		}
	}
	return false
}

func notSpace(r rune) bool {
	return !isSpace(r) // 独自実装のisSpaceを使用
}

src/pkg/log/log.goにおけるbytes.Bufferから[]byteへの変更

// 変更前 (概念)
// type Logger struct {
//     // ...
//     buf    bytes.Buffer // bytes.Bufferを使用
// }
//
// func itoa(buf *bytes.Buffer, i int, wid int) { // *bytes.Bufferを受け取る
//     // ...
//     buf.WriteByte('0') // bytes.BufferのWriteByteメソッド
//     // ...
// }
//
// func (l *Logger) Output(calldepth int, s string) error {
//     // ...
//     l.buf.Reset() // bytes.BufferのReset()
//     l.buf.WriteString(s) // bytes.BufferのWriteString()
//     // ...
//     _, err := l.out.Write(l.buf.Bytes()) // bytes.BufferのBytes()
//     // ...
// }

// 変更後
type Logger struct {
	// ...
	buf    []byte // bytes.Bufferから直接[]byteに変更
}

func itoa(buf *[]byte, i int, wid int) { // *[]byteを受け取るように変更
	var u uint = uint(i)
	if u == 0 && wid <= 1 {
		*buf = append(*buf, '0') // 直接appendを使用
		return
	}
	// ...
	*buf = append(*buf, b[bp:]...) // 直接appendを使用
}

func (l *Logger) Output(calldepth int, s string) error {
	// ...
	l.buf = l.buf[:0] // []byteスライスの長さをゼロにする (Reset()の代替)
	l.formatHeader(&l.buf, now, file, line) // []byteスライスへのポインタを渡す
	l.buf = append(l.buf, s...) // 直接appendを使用
	if len(s) > 0 && s[len(s)-1] != '\n' {
		l.buf = append(l.buf, '\n') // 直接appendを使用
	}
	_, err := l.out.Write(l.buf) // 直接[]byteスライスを書き込む
	return err
}

コアとなるコードの解説

このコミットの核となる変更は、fmtlogパッケージが持つ外部依存を削減し、パフォーマンスを向上させるための内部的な最適化です。

  1. unicodeパッケージの依存性排除とインライン化: fmt/scan.goでは、以前はunicode.IsSpace関数を使用して空白文字を判定していましたが、このコミットではunicodeパッケージのインポートを削除し、isSpaceという独自の関数を実装しました。このisSpace関数は、Goのunicodeパッケージが内部で持っているWhite_SpaceのUnicode範囲を直接コピーしたspaceという定数スライスを参照し、ルーンがその範囲内にあるかをチェックします。これにより、fmtパッケージがunicodeパッケージ全体を依存する必要がなくなり、バイナリサイズとコンパイル時間の削減に貢献します。これは、必要な機能がごく一部である場合に、より大きなライブラリ全体をインポートするオーバーヘッドを避けるための典型的な最適化手法です。

  2. bytes.Bufferから[]byteへの移行とカスタムバッファの実装: fmt/print.goでは、bytes.Bufferの代わりにbufferというカスタム型(実体は[]byteスライス)を導入し、WriteWriteStringWriteByteWriteRuneといったメソッドをこのカスタム型に実装しています。これらのメソッドは、内部の[]byteスライスに対して直接append関数を使用してデータを追加します。これにより、bytes.Bufferが提供する汎用的な機能のオーバーヘッドを避け、fmtパッケージが必要とする最小限のバッファ操作を直接[]byteスライスに対して行うことで、パフォーマンスの向上とメモリ使用量の最適化を図っています。特に、bytes.BufferReset()メソッドがp.buf = p.buf[:0]というスライス操作に置き換えられたことは、既存のメモリ領域を再利用しつつバッファをクリアする効率的な方法であり、不要なメモリ再割り当てを防ぎます。 同様の変更がlog/log.goでも行われ、Logger構造体のbufフィールドがbytes.Bufferから直接[]byteに変更され、関連する操作がappend関数とスライス操作に書き換えられています。これにより、logパッケージもbytes.Bufferへの依存を解消し、より効率的なログ出力が可能になります。

これらの変更は、Goの標準ライブラリが、パフォーマンスと依存関係の最小化を重視していることを明確に示しています。特に、コアとなるfmtlogのようなパッケージでは、わずかな依存関係の削減や最適化が、Goアプリケーション全体のパフォーマンスとバイナリサイズに大きな影響を与える可能性があります。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント (上記リンク)
  • Goのソースコード (コミット内容から直接読み取り)
  • Goのパッケージ設計に関する一般的な知識