[インデックス 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
このメッセージは、fmt
とlog
パッケージがunicode
およびunicode/utf8
に依存している現状を示し、その依存を停止するという意図を明確にしています。
変更の背景
Go言語の標準ライブラリは、その効率性と軽量性を重視して設計されています。特に、fmt
(フォーマット済みI/O)やlog
(ロギング)のようなコアパッケージは、多くのGoプログラムで利用されるため、その依存関係を最小限に抑えることが重要です。
unicode
パッケージは、広範なUnicode文字プロパティや操作を提供しますが、そのデータテーブルは比較的大規模になる可能性があります。fmt
やlog
のようなパッケージがunicode
パッケージの機能のごく一部(例えば、空白文字の判定など)しか必要としない場合、パッケージ全体をインポートすることは、不必要なバイナリサイズの増加やコンパイル時間の延長につながります。
同様に、bytes.Buffer
はバイトシーケンスを効率的に構築するための便利なツールですが、内部的なオーバーヘッドや汎用性ゆえに、特定の高性能なシナリオでは直接[]byte
スライスを操作する方がより効率的である場合があります。
このコミットは、これらの背景から、fmt
とlog
パッケージの依存関係を削減し、パフォーマンスを最適化するための具体的なステップとして実施されました。
前提知識の解説
このコミットの変更内容を理解するためには、以下の知識が役立ちます。
- 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
が最も基本的なパッケージ群を表し、L1
、L2
と続くにつれて、より高レベルな機能を持つパッケージが定義されます。この階層は、Goのビルドプロセスにおける依存関係の健全性を検証するために使用されます。
技術的詳細
このコミットは、fmt
とlog
パッケージの内部実装を大幅に変更し、外部依存を減らすことで、より自己完結型で効率的な動作を実現しています。
-
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.MaxRune
がutf8.MaxRune
に置き換えられています。これは、fmt
パッケージがUnicodeの全範囲ではなく、UTF-8で表現可能な最大ルーン値(U+10FFFF)のみを考慮すれば十分であることを示しています。
-
bytes.Buffer
から[]byte
への移行とカスタムバッファの実装:fmt/print.go
では、bytes.Buffer
の代わりにbuffer
という新しいカスタム型が導入されました。このbuffer
型は実質的に[]byte
スライスのエイリアスであり、Write
、WriteString
、WriteByte
、WriteRune
といったbytes.Buffer
と同様のメソッドが実装されています。これらのメソッドは、内部の[]byte
スライスに対して直接append
関数を使用してデータを追加します。これにより、bytes.Buffer
が持つ汎用的な機能のオーバーヘッドを避け、fmt
パッケージが必要とする最小限のバッファ操作を直接[]byte
スライスに対して行うことで、パフォーマンスの向上とメモリ使用量の最適化を図っています。bytes.Buffer
のReset()
メソッドは、スライスの長さをゼロにするp.buf = p.buf[:0]
に置き換えられています。これは、既存のメモリを再利用しつつバッファをクリアする効率的な方法です。bytes.Buffer
のWriteTo
メソッドの呼び出しも、w.Write(p.buf)
のように直接io.Writer
に[]byte
を書き込む形に変更されています。log/log.go
でも同様に、Logger
構造体のbuf
フィールドがbytes.Buffer
から直接[]byte
に変更されています。itoa
やformatHeader
といった内部関数は、*[]byte
ポインタを受け取るように変更され、append
関数を直接使用してバイトスライスにデータを追加するように書き換えられています。これにより、log
パッケージもbytes.Buffer
への依存を解消し、より効率的なログ出力が可能になります。
-
依存関係グラフの更新(
src/pkg/go/build/deps_test.go
):- このファイルは、Go標準ライブラリのパッケージ間の依存関係を定義しており、ビルドシステムが依存関係の健全性を検証するために使用されます。
fmt
とlog
パッケージがunicode
への依存を解消したことに伴い、このファイル内の依存関係定義も更新されました。特に、unicode
パッケージはL1
からL2
レベルに移動し、fmt
とlog
はL1
(およびその他の特定のパッケージ)に依存するように変更されました。これは、fmt
とlog
がより基本的な依存関係の階層に位置づけられることを意味し、Goのコアライブラリの軽量化戦略を反映しています。
これらの変更は、Goの標準ライブラリが、パフォーマンスと依存関係の最小化を重視していることを明確に示しています。特に、コアとなるfmt
やlog
のようなパッケージでは、わずかな依存関係の削減や最適化が、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
}
コアとなるコードの解説
このコミットの核となる変更は、fmt
とlog
パッケージが持つ外部依存を削減し、パフォーマンスを向上させるための内部的な最適化です。
-
unicode
パッケージの依存性排除とインライン化:fmt/scan.go
では、以前はunicode.IsSpace
関数を使用して空白文字を判定していましたが、このコミットではunicode
パッケージのインポートを削除し、isSpace
という独自の関数を実装しました。このisSpace
関数は、Goのunicode
パッケージが内部で持っているWhite_Space
のUnicode範囲を直接コピーしたspace
という定数スライスを参照し、ルーンがその範囲内にあるかをチェックします。これにより、fmt
パッケージがunicode
パッケージ全体を依存する必要がなくなり、バイナリサイズとコンパイル時間の削減に貢献します。これは、必要な機能がごく一部である場合に、より大きなライブラリ全体をインポートするオーバーヘッドを避けるための典型的な最適化手法です。 -
bytes.Buffer
から[]byte
への移行とカスタムバッファの実装:fmt/print.go
では、bytes.Buffer
の代わりにbuffer
というカスタム型(実体は[]byte
スライス)を導入し、Write
、WriteString
、WriteByte
、WriteRune
といったメソッドをこのカスタム型に実装しています。これらのメソッドは、内部の[]byte
スライスに対して直接append
関数を使用してデータを追加します。これにより、bytes.Buffer
が提供する汎用的な機能のオーバーヘッドを避け、fmt
パッケージが必要とする最小限のバッファ操作を直接[]byte
スライスに対して行うことで、パフォーマンスの向上とメモリ使用量の最適化を図っています。特に、bytes.Buffer
のReset()
メソッドがp.buf = p.buf[:0]
というスライス操作に置き換えられたことは、既存のメモリ領域を再利用しつつバッファをクリアする効率的な方法であり、不要なメモリ再割り当てを防ぎます。 同様の変更がlog/log.go
でも行われ、Logger
構造体のbuf
フィールドがbytes.Buffer
から直接[]byte
に変更され、関連する操作がappend
関数とスライス操作に書き換えられています。これにより、log
パッケージもbytes.Buffer
への依存を解消し、より効率的なログ出力が可能になります。
これらの変更は、Goの標準ライブラリが、パフォーマンスと依存関係の最小化を重視していることを明確に示しています。特に、コアとなるfmt
やlog
のようなパッケージでは、わずかな依存関係の削減や最適化が、Goアプリケーション全体のパフォーマンスとバイナリサイズに大きな影響を与える可能性があります。
関連リンク
- Go
unicode
package documentation: https://pkg.go.dev/unicode - Go
bytes
package documentation: https://pkg.go.dev/bytes - Go
fmt
package documentation: https://pkg.go.dev/fmt - Go
log
package documentation: https://pkg.go.dev/log - Go
strconv
package documentation: https://pkg.go.dev/strconv - Go
utf8
package documentation: https://pkg.go.dev/unicode/utf8
参考にした情報源リンク
- Goの公式ドキュメント (上記リンク)
- Goのソースコード (コミット内容から直接読み取り)
- Goのパッケージ設計に関する一般的な知識