[インデックス 10630] ファイルの概要
このコミットは、Go言語の標準ライブラリである strconv
パッケージ内の整数(int
および uint
)の文字列フォーマット処理を改善するものです。具体的には、FormatInt
、AppendInt
、そして内部で利用される formatBits
関数のコードを簡素化し、同時にパフォーマンスをわずかに向上させています。
コミット
commit b219e8cbcf67e10b47ab6ebe97eb6497f6010000
Author: Robert Griesemer <gri@golang.org>
Date: Tue Dec 6 13:54:22 2011 -0800
strconv: squeezed a bit more out of int/uint formatting
- less code
- slightly better performance (0-4%)
R=r, rsc
CC=golang-dev
https://golang.org/cl/5448120
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b219e8cbcf67e10b47ab6ebe97eb6497f6010000
元コミット内容
strconv: squeezed a bit more out of int/uint formatting
(strconv: int/uintのフォーマットからもう少し絞り出した)
less code
(コード量の削減)slightly better performance (0-4%)
(わずかなパフォーマンス向上 (0-4%))
変更の背景
Go言語の strconv
パッケージは、プリミティブ型と文字列との間の変換を提供します。特に、整数を文字列に変換する処理(FormatInt
, FormatUint
, Itoa
など)は、多くのアプリケーションで頻繁に利用される基本的な機能です。このような基盤となる機能のパフォーマンスは、Goプログラム全体の実行速度に大きな影響を与える可能性があります。
このコミットの背景には、以下の目的があったと考えられます。
- コードの簡素化と保守性の向上: 整数フォーマットのロジックは、符号の有無、基数(10進数、2進数、16進数など)、そしてゼロの特殊処理など、考慮すべき点が多岐にわたります。これらのロジックをより統一的かつ簡潔に記述することで、コードの理解しやすさ、テストのしやすさ、将来の変更に対する堅牢性を高めることができます。
- パフォーマンスの最適化: 整数から文字列への変換は、内部で除算や剰余演算を伴うことが多く、これらの演算は比較的コストが高いです。特に、ループ内でこれらの演算が繰り返される場合、わずかな改善でも全体的なパフォーマンスに寄与します。コミットメッセージにある「0-4%」という改善は、一見小さく見えますが、高頻度で呼び出される関数においては無視できない効果をもたらします。これは、Go言語がパフォーマンスを重視する設計思想を持っていることの表れでもあります。
前提知識の解説
strconv
パッケージ
strconv
パッケージは、Go言語の標準ライブラリの一部であり、文字列と基本的なデータ型(ブール値、整数、浮動小数点数)との間の変換機能を提供します。例えば、strconv.Itoa
は int
を文字列に変換し、strconv.Atoi
は文字列を int
に変換します。これらの関数は、コマンドライン引数のパース、設定ファイルの読み込み、ネットワークプロトコルの処理など、様々な場面で利用されます。
整数から文字列への変換アルゴリズム
整数を文字列に変換する一般的なアルゴリズムは、基数変換に基づいています。例えば、10進数の整数を文字列に変換する場合、以下の手順を踏みます。
- 剰余演算: 整数を基数(例: 10)で割った剰余を求めます。これが最下位の桁の数字になります。
- 除算: 整数を基数で割った商を求め、これを次の処理の対象とします。
- 繰り返し: 商が0になるまで1と2のステップを繰り返します。
- 文字列の構築: 求めた桁の数字を逆順に並べることで、文字列が得られます。
負の数の場合、通常は絶対値を変換し、最後に先頭にマイナス記号を追加します。
パフォーマンス最適化の考慮点
- 除算と剰余演算: これらの演算はCPUにとって比較的重い処理です。特に、コンパイラが最適化できないような動的な基数での除算はコストが高くなります。
- メモリ割り当て: 文字列は不変であるため、文字列を構築する際には新しいメモリが割り当てられることがあります。特に、
append
を使用してバイトスライスを構築し、最後に文字列に変換するアプローチは、メモリ割り当ての回数を減らし、パフォーマンスを向上させるための一般的な手法です。 - 分岐予測:
if
文やループの条件分岐が多いと、CPUの分岐予測が失敗し、パイプラインのストールを引き起こす可能性があります。コードパスを統一することで、分岐予測の精度を向上させ、パフォーマンスを改善できる場合があります。 - 特殊ケースの処理: ゼロや特定の基数(例: 2のべき乗)など、特定の入力に対して特殊な処理を行うことで、効率を向上させることがあります。しかし、特殊ケースの処理が多すぎると、コードが複雑になり、かえってオーバーヘッドが増える可能性もあります。
技術的詳細
このコミットの主要な変更点は、src/pkg/strconv/itoa.go
ファイル内の formatBits
関数のロジックの簡素化と最適化にあります。
1. signed
パラメータから negative
パラメータへの変更
- 変更前:
func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, s string)
signed
は、u
が符号付き整数として扱われるべきかを示す汎用的なフラグでした。
- 変更後:
func formatBits(dst []byte, u uint64, base int, negative, append_ bool) (d []byte, s string)
negative
は、元の整数が負であったかどうかを直接示す具体的なフラグになりました。
この変更により、FormatInt
および AppendInt
関数からの呼び出しで、uint64(i)
を formatBits
に渡す際に、元の int64
の符号 (i < 0
) を直接 negative
パラメータとして渡すようになりました。これにより、formatBits
内部で int64(u)
への型変換を行って符号を判断する必要がなくなり、コードがより直接的で明確になりました。
2. ゼロ (u == 0
) の特殊処理の削除
- 変更前は、
u == 0
の場合に'0'
を返す特殊なif
ブロックが存在しました。 - 変更後、この特殊処理が削除されました。
これは、メインの変換ロジックがゼロを正しく処理できるように改善されたことを意味します。これにより、コードパスが統一され、コード量が削減されました。
3. 負の数の処理ロジックの簡素化
- 変更前:
x := int64(u) if x < 0 && signed { u = -u }
uint64
をint64
に変換し、その値とsigned
フラグを組み合わせて負の数を判断していました。
- 変更後:
if negative { u = -u }
- 新しい
negative
パラメータを直接利用することで、不要な型変換と条件分岐が削除されました。これにより、負の数の絶対値を取得するロジックがより効率的になりました。
- 新しい
4. ループ条件の変更と最後の桁の明示的な処理
これはパフォーマンス向上に最も寄与する変更点の一つです。
- 変更前: 変換ループは
u != 0
が条件でした。つまり、u
が0になるまでループが続きました。 - 変更後: 変換ループの条件が
u >= base
(またはu >= 10
for base 10) に変更されました。- これにより、ループは最後の1桁(
u < base
となる場合)を残して終了します。 - ループの直後に、残った最後の1桁を明示的に処理するコードが追加されました。
// u < base i-- a[i] = digits[uintptr(u)]
- これにより、ループは最後の1桁(
この変更の利点は以下の通りです。
- ループ回数の最適化: 多くの数値において、ループの最終イテレーションで
u
が0
になる直前の処理が、ループ外で一度だけ行われるようになりました。これにより、ループ内の条件チェックや分岐のオーバーヘッドがわずかに削減される可能性があります。 - ゼロの統一処理: ゼロの特殊処理を削除できたのは、この新しいループ条件と最後の桁の処理のおかげです。
u
が最初から0
の場合、ループは実行されず、直接a[i] = digits[uintptr(u)]
(つまりdigits[0]
) が実行され、正しく'0'
が書き込まれます。 - 分岐予測の改善: ループ内の条件分岐が減ることで、CPUの分岐予測がより正確になり、パイプラインの効率が向上する可能性があります。
5. 2のべき乗基数での最適化の微調整
- 変更前:
m := uintptr(1)<<s - 1
- 変更後:
m := uintptr(b) - 1 // == 1<<s - 1
- コメントが追加され、
b
(基数) を使ってm
を計算する意図が明確になりました。これは機能的な変更ではなく、コードの可読性と意図の明確化を目的としています。
- コメントが追加され、
これらの変更は全体として、コードの重複を減らし、より統一されたロジックパスを提供することで、「コード量の削減」と「パフォーマンスの向上」という目標を達成しています。特に、ゼロの特殊処理の削除とループ条件の変更は、Go言語の strconv
パッケージが、数値変換の効率を追求する上でいかに細部にまでこだわっているかを示しています。
コアとなるコードの変更箇所
src/pkg/strconv/itoa.go
ファイルにおいて、以下の変更が行われました。
FormatInt
関数:--- a/src/pkg/strconv/itoa.go +++ b/src/pkg/strconv/itoa.go @@ -12,7 +12,7 @@ func FormatUint(i uint64, base int) string { // FormatInt returns the string representation of i in the given base. func FormatInt(i int64, base int) string { - _, s := formatBits(nil, uint64(i), base, true, false) + _, s := formatBits(nil, uint64(i), base, i < 0, false) return s }
AppendInt
関数:--- a/src/pkg/strconv/itoa.go +++ b/src/pkg/strconv/itoa.go @@ -24,7 +24,7 @@ func Itoa(i int) string { // AppendInt appends the string form of the integer i, // as generated by FormatInt, to dst and returns the extended buffer. func AppendInt(dst []byte, i int64, base int) []byte { - dst, _ = formatBits(dst, uint64(i), base, true, true) + dst, _ = formatBits(dst, uint64(i), base, i < 0, true) return dst }
formatBits
関数のシグネチャと内部ロジック:--- a/src/pkg/strconv/itoa.go +++ b/src/pkg/strconv/itoa.go @@ -46,31 +46,21 @@ var shifts = [len(digits) + 1]uint{ } // formatBits computes the string representation of u in the given base. -// If signed is set, u is treated as int64 value. If append_ is set, the -// string is appended to dst and the resulting byte slice is returned as -// the first result value; otherwise the string is simply returned as the -// second result value.\n +// If negative is set, u is treated as negative int64 value. If append_ +// is set, the string is appended to dst and the resulting byte slice is +// returned as the first result value; otherwise the string is returned +// as the second result value. // -func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, s string) { +func formatBits(dst []byte, u uint64, base int, negative, append_ bool) (d []byte, s string) { if base < 2 || base > len(digits) { panic("invalid base") } // 2 <= base && base <= len(digits) - if u == 0 { - if append_ { - d = append(dst, '0') - return - } - s = "0" - return - } - var a [64 + 1]byte // +1 for sign of 64bit value in base 2 i := len(a) - x := int64(u) - if x < 0 && signed { + if negative { u = -u } @@ -78,7 +68,7 @@ func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, if base == 10 { // common case: use constant 10 for / and % because // the compiler can optimize it into a multiply+shift - for u != 0 { + for u >= 10 { \ti-- \ta[i] = digits[u%10] \tu /= 10 @@ -86,8 +76,9 @@ func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, } else if s := shifts[base]; s > 0 { // base is power of 2: use shifts and masks instead of / and % - m := uintptr(1)<<s - 1 - for u != 0 {\n + b := uint64(base) + m := uintptr(b) - 1 // == 1<<s - 1 + for u >= b { \ti-- \ta[i] = digits[uintptr(u)&m] \tu >>= s @@ -96,15 +96,19 @@ func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, } else { // general case b := uint64(base) - for u != 0 { + for u >= b { \ti-- \ta[i] = digits[u%b] \tu /= b } } +\t// u < base +\ti-- +\ta[i] = digits[uintptr(u)] +\n // add sign, if any - if x < 0 && signed { + if negative { i-- a[i] = '-' }
コアとなるコードの解説
このコミットの核心は、formatBits
関数の内部ロジックの変更にあります。
-
formatBits
関数のシグネチャ変更:signed
という汎用的なブール値のパラメータがnegative
というより具体的なパラメータに置き換えられました。これにより、関数を呼び出す側(FormatInt
やAppendInt
)は、元の整数が負であったかどうかを直接negative
に渡すことができるようになり、formatBits
内部での符号判定ロジックが簡素化されました。 -
ゼロの特殊処理の削除: 変更前は、入力
u
が0
の場合に'0'
を直接返すための特別なif
ブロックがありました。このコミットでは、このブロックが削除されました。これは、後述するループ条件の変更と最後の桁の処理によって、ゼロが一般の数値と同様に正しく処理されるようになったためです。これにより、コードの重複が排除され、コードパスが統一されました。 -
負の数の絶対値化ロジックの簡素化: 変更前は、
uint64
型のu
をint64
にキャストし、その結果とsigned
フラグを組み合わせて負の数を判断していました。変更後は、新しいnegative
パラメータを直接利用してu = -u
を実行するだけになりました。これにより、不要な型変換と複雑な条件が取り除かれ、コードがより効率的かつ読みやすくなりました。 -
数値変換ループの最適化: これが最も重要な変更点です。
- 従来のループ条件は
u != 0
でした。これは、u
が0
になるまで(つまり、すべての桁が処理されるまで)ループを繰り返すことを意味します。 - 新しいループ条件は
u >= base
(またはu >= 10
for base 10) です。この条件により、ループはu
がbase
よりも小さくなった時点で終了します。これは、数値の最上位の桁がまだu
に残っている状態です。 - ループが終了した後、残った
u
(これはbase
未満の値、つまり最後の1桁) を明示的に処理する行が追加されました。// u < base i-- a[i] = digits[uintptr(u)]
この変更により、ループのイテレーション回数が1回減る可能性があります(特に1桁の数値や、最後の桁が処理された直後に
u
が0
になる場合)。また、ループ内の条件分岐がよりシンプルになり、CPUの分岐予測が改善されることで、全体的なパフォーマンスが向上すると考えられます。ゼロの特殊処理を削除できたのも、この新しいループ構造がゼロを自然に処理できるようになったためです。 - 従来のループ条件は
これらの変更は、Go言語の strconv
パッケージが、数値変換という基本的な操作においても、コードの簡潔さと実行効率の両方を追求していることを示しています。
関連リンク
- Go言語
strconv
パッケージのドキュメント: https://pkg.go.dev/strconv - Go言語のソースコードリポジトリ: https://github.com/golang/go
- このコミットが属するGerritの変更リスト: https://golang.org/cl/5448120
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (
src/pkg/strconv/itoa.go
) - 一般的な整数から文字列への変換アルゴリズムに関する情報
- CPUの分岐予測とパイプラインに関する一般的な知識
- Go言語のパフォーマンス最適化に関する記事 (一般的な知識として)
[インデックス 10630] ファイルの概要
このコミットは、Go言語の標準ライブラリである strconv
パッケージ内の整数(int
および uint
)の文字列フォーマット処理を改善するものです。具体的には、FormatInt
、AppendInt
、そして内部で利用される formatBits
関数のコードを簡素化し、同時にパフォーマンスをわずかに向上させています。
コミット
commit b219e8cbcf67e10b47ab6ebe97eb6497f6010000
Author: Robert Griesemer <gri@golang.org>
Date: Tue Dec 6 13:54:22 2011 -0800
strconv: squeezed a bit more out of int/uint formatting
- less code
- slightly better performance (0-4%)
R=r, rsc
CC=golang-dev
https://golang.org/cl/5448120
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b219e8cbcf67e10b47ab6ebe97eb6497f6010000
元コミット内容
strconv: squeezed a bit more out of int/uint formatting
(strconv: int/uintのフォーマットからもう少し絞り出した)
less code
(コード量の削減)slightly better performance (0-4%)
(わずかなパフォーマンス向上 (0-4%))
変更の背景
Go言語の strconv
パッケージは、プリミティブ型と文字列との間の変換を提供します。特に、整数を文字列に変換する処理(FormatInt
, FormatUint
, Itoa
など)は、多くのアプリケーションで頻繁に利用される基本的な機能です。このような基盤となる機能のパフォーマンスは、Goプログラム全体の実行速度に大きな影響を与える可能性があります。
このコミットの背景には、以下の目的があったと考えられます。
- コードの簡素化と保守性の向上: 整数フォーマットのロジックは、符号の有無、基数(10進数、2進数、16進数など)、そしてゼロの特殊処理など、考慮すべき点が多岐にわたります。これらのロジックをより統一的かつ簡潔に記述することで、コードの理解しやすさ、テストのしやすさ、将来の変更に対する堅牢性を高めることができます。
- パフォーマンスの最適化: 整数から文字列への変換は、内部で除算や剰余演算を伴うことが多く、これらの演算は比較的コストが高いです。特に、ループ内でこれらの演算が繰り返される場合、わずかな改善でも全体的なパフォーマンスに寄与します。コミットメッセージにある「0-4%」という改善は、一見小さく見えますが、高頻度で呼び出される関数においては無視できない効果をもたらします。これは、Go言語がパフォーマンスを重視する設計思想を持っていることの表れでもあります。
前提知識の解説
strconv
パッケージ
strconv
パッケージは、Go言語の標準ライブラリの一部であり、文字列と基本的なデータ型(ブール値、整数、浮動小数点数)との間の変換機能を提供します。例えば、strconv.Itoa
は int
を文字列に変換し、strconv.Atoi
は文字列を int
に変換します。これらの関数は、コマンドライン引数のパース、設定ファイルの読み込み、ネットワークプロトコルの処理など、様々な場面で利用されます。
整数から文字列への変換アルゴリズム
整数を文字列に変換する一般的なアルゴリズムは、基数変換に基づいています。例えば、10進数の整数を文字列に変換する場合、以下の手順を踏みます。
- 剰余演算: 整数を基数(例: 10)で割った剰余を求めます。これが最下位の桁の数字になります。
- 除算: 整数を基数で割った商を求め、これを次の処理の対象とします。
- 繰り返し: 商が0になるまで1と2のステップを繰り返します。
- 文字列の構築: 求めた桁の数字を逆順に並べることで、文字列が得られます。
負の数の場合、通常は絶対値を変換し、最後に先頭にマイナス記号を追加します。
パフォーマンス最適化の考慮点
- 除算と剰余演算: これらの演算はCPUにとって比較的重い処理です。特に、コンパイラが最適化できないような動的な基数での除算はコストが高くなります。
- メモリ割り当て: 文字列は不変であるため、文字列を構築する際には新しいメモリが割り当てられることがあります。特に、
append
を使用してバイトスライスを構築し、最後に文字列に変換するアプローチは、メモリ割り当ての回数を減らし、パフォーマンスを向上させるための一般的な手法です。 - 分岐予測:
if
文やループの条件分岐が多いと、CPUの分岐予測が失敗し、パイプラインのストールを引き起こす可能性があります。コードパスを統一することで、分岐予測の精度を向上させ、パフォーマンスを改善できる場合があります。 - 特殊ケースの処理: ゼロや特定の基数(例: 2のべき乗)など、特定の入力に対して特殊な処理を行うことで、効率を向上させることがあります。しかし、特殊ケースの処理が多すぎると、コードが複雑になり、かえってオーバーヘッドが増える可能性もあります。
strconv
パッケージは、これらの考慮点を踏まえて高度に最適化されています。例えば、strconv.Itoa
は int
から string
への変換に特化しており、非常に効率的です。fmt.Sprintf
と比較して、strconv
は内部でリフレクションを使用せず、より少ないメモリ割り当てで処理を行います。また、小さな整数に対しては事前に計算された文字列を返す「高速パス」を持っていたり、2桁ずつ処理することで除算・剰余演算の回数を減らしたり、2のべき乗の基数(バイナリ、16進数など)に対してはビット演算を利用したりするなど、様々な最適化が施されています。
技術的詳細
このコミットの主要な変更点は、src/pkg/strconv/itoa.go
ファイル内の formatBits
関数のロジックの簡素化と最適化にあります。
1. signed
パラメータから negative
パラメータへの変更
- 変更前:
func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, s string)
signed
は、u
が符号付き整数として扱われるべきかを示す汎用的なフラグでした。
- 変更後:
func formatBits(dst []byte, u uint64, base int, negative, append_ bool) (d []byte, s string)
negative
は、元の整数が負であったかどうかを直接示す具体的なフラグになりました。
この変更により、FormatInt
および AppendInt
関数からの呼び出しで、uint64(i)
を formatBits
に渡す際に、元の int64
の符号 (i < 0
) を直接 negative
パラメータとして渡すようになりました。これにより、formatBits
内部で int64(u)
への型変換を行って符号を判断する必要がなくなり、コードがより直接的で明確になりました。
2. ゼロ (u == 0
) の特殊処理の削除
- 変更前は、
u == 0
の場合に'0'
を返す特殊なif
ブロックが存在しました。 - 変更後、この特殊処理が削除されました。
これは、メインの変換ロジックがゼロを正しく処理できるように改善されたことを意味します。これにより、コードパスが統一され、コード量が削減されました。
3. 負の数の処理ロジックの簡素化
- 変更前:
x := int64(u) if x < 0 && signed { u = -u }
uint64
をint64
に変換し、その値とsigned
フラグを組み合わせて負の数を判断していました。
- 変更後:
if negative { u = -u }
- 新しい
negative
パラメータを直接利用することで、不要な型変換と条件分岐が削除されました。これにより、負の数の絶対値を取得するロジックがより効率的になりました。
- 新しい
4. ループ条件の変更と最後の桁の明示的な処理
これはパフォーマンス向上に最も寄与する変更点の一つです。
- 変更前: 変換ループは
u != 0
が条件でした。つまり、u
が0になるまでループが続きました。 - 変更後: 変換ループの条件が
u >= base
(またはu >= 10
for base 10) に変更されました。- これにより、ループは最後の1桁(
u < base
となる場合)を残して終了します。 - ループの直後に、残った最後の1桁を明示的に処理するコードが追加されました。
// u < base i-- a[i] = digits[uintptr(u)]
- これにより、ループは最後の1桁(
この変更の利点は以下の通りです。
- ループ回数の最適化: 多くの数値において、ループの最終イテレーションで
u
が0
になる直前の処理が、ループ外で一度だけ行われるようになりました。これにより、ループ内の条件チェックや分岐のオーバーヘッドがわずかに削減される可能性があります。 - ゼロの統一処理: ゼロの特殊処理を削除できたのは、この新しいループ条件と最後の桁の処理のおかげです。
u
が最初から0
の場合、ループは実行されず、直接a[i] = digits[uintptr(u)]
(つまりdigits[0]
) が実行され、正しく'0'
が書き込まれます。 - 分岐予測の改善: ループ内の条件分岐が減ることで、CPUの分岐予測がより正確になり、パイプラインの効率が向上する可能性があります。
5. 2のべき乗基数での最適化の微調整
- 変更前:
m := uintptr(1)<<s - 1
- 変更後:
m := uintptr(b) - 1 // == 1<<s - 1
- コメントが追加され、
b
(基数) を使ってm
を計算する意図が明確になりました。これは機能的な変更ではなく、コードの可読性と意図の明確化を目的としています。
- コメントが追加され、
これらの変更は全体として、コードの重複を減らし、より統一されたロジックパスを提供することで、「コード量の削減」と「パフォーマンスの向上」という目標を達成しています。特に、ゼロの特殊処理の削除とループ条件の変更は、Go言語の strconv
パッケージが、数値変換の効率を追求する上でいかに細部にまでこだわっているかを示しています。
コアとなるコードの変更箇所
src/pkg/strconv/itoa.go
ファイルにおいて、以下の変更が行われました。
FormatInt
関数:--- a/src/pkg/strconv/itoa.go +++ b/src/pkg/strconv/itoa.go @@ -12,7 +12,7 @@ func FormatUint(i uint64, base int) string { // FormatInt returns the string representation of i in the given base. func FormatInt(i int64, base int) string { - _, s := formatBits(nil, uint64(i), base, true, false) + _, s := formatBits(nil, uint64(i), base, i < 0, false) return s }
AppendInt
関数:--- a/src/pkg/strconv/itoa.go +++ b/src/pkg/strconv/itoa.go @@ -24,7 +24,7 @@ func Itoa(i int) string { // AppendInt appends the string form of the integer i, // as generated by FormatInt, to dst and returns the extended buffer. func AppendInt(dst []byte, i int64, base int) []byte { - dst, _ = formatBits(dst, uint64(i), base, true, true) + dst, _ = formatBits(dst, uint64(i), base, i < 0, true) return dst }
formatBits
関数のシグネチャと内部ロジック:--- a/src/pkg/strconv/itoa.go +++ b/src/pkg/strconv/itoa.go @@ -46,31 +46,21 @@ var shifts = [len(digits) + 1]uint{ } // formatBits computes the string representation of u in the given base. -// If signed is set, u is treated as int64 value. If append_ is set, the -// string is appended to dst and the resulting byte slice is returned as -// the first result value; otherwise the string is simply returned as the -// second result value.\n +// If negative is set, u is treated as negative int64 value. If append_ +// is set, the string is appended to dst and the resulting byte slice is +// returned as the first result value; otherwise the string is returned +// as the second result value. // -func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, s string) { +func formatBits(dst []byte, u uint64, base int, negative, append_ bool) (d []byte, s string) { if base < 2 || base > len(digits) { panic("invalid base") } // 2 <= base && base <= len(digits) - if u == 0 { - if append_ { - d = append(dst, '0') - return - } - s = "0" - return - } - var a [64 + 1]byte // +1 for sign of 64bit value in base 2 i := len(a) - x := int64(u) - if x < 0 && signed { + if negative { u = -u } @@ -78,7 +68,7 @@ func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, if base == 10 { // common case: use constant 10 for / and % because // the compiler can optimize it into a multiply+shift - for u != 0 { + for u >= 10 { \ti-- \ta[i] = digits[u%10] \tu /= 10 @@ -86,8 +76,9 @@ func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, } else if s := shifts[base]; s > 0 { // base is power of 2: use shifts and masks instead of / and % - m := uintptr(1)<<s - 1 - for u != 0{\n + b := uint64(base) + m := uintptr(b) - 1 // == 1<<s - 1 + for u >= b { \ti-- \ta[i] = digits[uintptr(u)&m] \tu >>= s @@ -96,15 +87,19 @@ func formatBits(dst []byte, u uint64, base int, signed, append_ bool) (d []byte, } else { // general case b := uint64(base) - for u != 0 { + for u >= b { \ti-- \ta[i] = digits[u%b] \tu /= b } } +\t// u < base +\ti-- +\ta[i] = digits[uintptr(u)] +\n // add sign, if any - if x < 0 && signed { + if negative { i-- a[i] = '-' }
コアとなるコードの解説
このコミットの核心は、formatBits
関数の内部ロジックの変更にあります。
-
formatBits
関数のシグネチャ変更:signed
という汎用的なブール値のパラメータがnegative
というより具体的なパラメータに置き換えられました。これにより、関数を呼び出す側(FormatInt
やAppendInt
)は、元の整数が負であったかどうかを直接negative
に渡すことができるようになり、formatBits
内部での符号判定ロジックが簡素化されました。 -
ゼロの特殊処理の削除: 変更前は、入力
u
が0
の場合に'0'
を直接返すための特別なif
ブロックがありました。このコミットでは、このブロックが削除されました。これは、後述するループ条件の変更と最後の桁の処理によって、ゼロが一般の数値と同様に正しく処理されるようになったためです。これにより、コードの重複が排除され、コードパスが統一されました。 -
負の数の絶対値化ロジックの簡素化: 変更前は、
uint64
型のu
をint64
にキャストし、その結果とsigned
フラグを組み合わせて負の数を判断していました。変更後は、新しいnegative
パラメータを直接利用してu = -u
を実行するだけになりました。これにより、不要な型変換と複雑な条件が取り除かれ、コードがより効率的かつ読みやすくなりました。 -
数値変換ループの最適化: これが最も重要な変更点です。
- 従来のループ条件は
u != 0
でした。これは、u
が0
になるまで(つまり、すべての桁が処理されるまで)ループを繰り返すことを意味します。 - 新しいループ条件は
u >= base
(またはu >= 10
for base 10) です。この条件により、ループはu
がbase
よりも小さくなった時点で終了します。これは、数値の最上位の桁がまだu
に残っている状態です。 - ループが終了した後、残った
u
(これはbase
未満の値、つまり最後の1桁) を明示的に処理する行が追加されました。// u < base i-- a[i] = digits[uintptr(u)]
この変更により、ループのイテレーション回数が1回減る可能性があります(特に1桁の数値や、最後の桁が処理された直後に
u
が0
になる場合)。また、ループ内の条件分岐がよりシンプルになり、CPUの分岐予測が改善されることで、全体的なパフォーマンスが向上すると考えられます。ゼロの特殊処理を削除できたのも、この新しいループ構造がゼロを自然に処理できるようになったためです。 - 従来のループ条件は
これらの変更は、Go言語の strconv
パッケージが、数値変換という基本的な操作においても、コードの簡潔さと実行効率の両方を追求していることを示しています。
関連リンク
- Go言語
strconv
パッケージのドキュメント: https://pkg.go.dev/strconv - Go言語のソースコードリポジトリ: https://github.com/golang/go
- このコミットが属するGerritの変更リスト: https://golang.org/cl/5448120
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (
src/pkg/strconv/itoa.go
) - 一般的な整数から文字列への変換アルゴリズムに関する情報
- CPUの分岐予測とパイプラインに関する一般的な知識
- Go言語のパフォーマンス最適化に関する記事 (一般的な知識として)
- Web検索結果: "Go strconv package performance optimization integer to string" (特に、
strconv
がどのように最適化されているかに関する情報)