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

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

このコミットは、Go言語の標準ライブラリである unicode/utf8 パッケージ内の utf8.go ファイルに対する変更です。unicode/utf8 パッケージは、UTF-8エンコーディングされたテキストを操作するためのユーティリティを提供します。具体的には、EncodeRune 関数におけるコードの簡素化が行われています。

コミット

unicode/utf8 パッケージの EncodeRune 関数における、サロゲートコードポイントのチェックに関する冗長なロジックを削除し、コードを簡素化しました。RuneError はサロゲートコードポイントではないため、RuneError が割り当てられた後にサロゲートコードポイントであるかどうかのチェックは不要であるという最適化です。

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

https://github.com/golang/go/commit/446d90d727b1820f8f4ef2f4e22d6ce1cd88df4d

元コミット内容

unicode/utf8: minor code simplification

It's a little bit waste to check if r is not a surrogate
code point because RuneError is not a surrogate code point.

LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/79230043

変更の背景

この変更の背景には、EncodeRune 関数内の既存のロジックにおける冗長性の排除があります。EncodeRune 関数は、与えられた rune (Unicodeコードポイント) をUTF-8バイト列にエンコードする役割を担っています。この関数は、無効な rune 値(例えば、Unicodeの最大値 MaxRune を超える値や、UTF-8では無効とされるサロゲートコードポイントの範囲内の値)を受け取った場合、RuneError (U+FFFD REPLACEMENT CHARACTER) に置き換えるという処理を行います。

元のコードでは、runeMaxRune を超える場合に RuneError を割り当てた後、さらにその rune がサロゲートコードポイントの範囲内にあるかどうかのチェックを行っていました。しかし、RuneError 自体はサロゲートコードポイントの範囲外にあることが保証されています。したがって、RuneError が既に割り当てられている状況で、再度サロゲートコードポイントのチェックを行うことは無駄であり、冗長な処理となっていました。

このコミットは、この冗長なチェックを排除し、コードの可読性と効率性を向上させることを目的としています。

前提知識の解説

UTF-8エンコーディング

UTF-8 (Unicode Transformation Format - 8-bit) は、Unicode文字を可変長のバイト列で表現するエンコーディング方式です。

  • 可変長エンコーディング: 1文字を表現するのに1バイトから4バイトを使用します。
  • ASCII互換性: 最初の128文字(U+0000からU+007F、ASCII文字)は1バイトで表現され、ASCIIと完全に互換性があります。
  • バイトシーケンス:
    • 1バイト文字: 0xxxxxxx
    • 2バイト文字: 110xxxxx 10xxxxxx
    • 3バイト文字: 1110xxxx 10xxxxxx 10xxxxxx
    • 4バイト文字: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ここで x はUnicodeコードポイントのビットを表します。

Go言語における rune

Go言語では、文字列はUTF-8バイト列として扱われます。string 型は読み取り専用のバイトスライスです。 一方、rune 型はGoに特有の型で、Unicodeコードポイントを表します。runeint32 のエイリアスであり、単一のUnicode文字に対応します。 for...range ループで文字列をイテレートすると、各 rune とその開始バイトインデックスが返されます。

サロゲートコードポイント (Surrogate Code Points)

サロゲートコードポイントは、UnicodeのU+D800からU+DFFFまでの範囲にあるコードポイントです。この範囲は、UTF-16エンコーディングにおいて、基本多言語面 (BMP: Basic Multilingual Plane, U+0000からU+FFFF) 外の文字(補助文字)を表現するために予約されています。

  • UTF-16での使用: UTF-16では、補助文字を「サロゲートペア」(高サロゲートと低サロゲートの2つの16ビットコードユニットの組み合わせ)としてエンコードします。
  • UTF-8での扱い: UTF-8では、サロゲートコードポイントは直接エンコードされません。UTF-8は補助文字を直接4バイトシーケンスとしてエンコードします。したがって、有効なUTF-8バイト列にはサロゲートコードポイントが含まれていてはなりません。もしUTF-8ストリーム中にサロゲートコードポイントが検出された場合、それは無効なシーケンスと見なされます。

RuneError

RuneError は、Goの unicode/utf8 パッケージで定義されている定数で、Unicodeの置換文字 (REPLACEMENT CHARACTER, U+FFFD) を表します。 これは、UTF-8デコード関数(例: utf8.DecodeRune)が無効な、または不正なUTF-8バイトシーケンスを検出した場合に返される値です。エンコード関数(例: utf8.EncodeRune)においても、無効な rune が与えられた場合に、この RuneError に置き換えられて処理されます。 RuneError (U+FFFD) は、サロゲートコードポイントの範囲 (U+D800からU+DFFF) 外に位置します。

unicode/utf8 パッケージ

Go言語の unicode/utf8 パッケージは、UTF-8エンコーディングされたテキストを扱うための基本的な関数を提供します。これには、rune とバイトスライスの間のエンコード/デコード、UTF-8バイト列の検証、rune のカウントなどが含まれます。

技術的詳細

EncodeRune 関数は、rune 型のUnicodeコードポイント r を受け取り、それをUTF-8バイト列に変換してバイトスライス p に書き込みます。そして、書き込んだバイト数を返します。

元のコードのロジックは、if-else if の連鎖で、rune の値に基づいて適切なUTF-8エンコーディングのバイト数を決定し、バイト列を生成していました。

  1. 1バイト文字: rrune1Max (U+007F) 以下の場合。
  2. 2バイト文字: rrune2Max (U+07FF) 以下の場合。
  3. エラー処理: rMaxRune (U+10FFFF) を超える場合、またはサロゲートコードポイントの範囲内 (surrogateMin から surrogateMax) にある場合、rRuneError に置き換えます。
  4. 3バイト文字: rrune3Max (U+FFFF) 以下の場合。
  5. 4バイト文字: それ以外の場合(補助文字)。

問題は、ステップ3のエラー処理部分にありました。

if uint32(r) > MaxRune {
	r = RuneError
}

if surrogateMin <= r && r <= surrogateMax { // ここが冗長
	r = RuneError
}

もし rMaxRune を超えて RuneError に置き換えられた場合、その後の if surrogateMin <= r && r <= surrogateMax のチェックは常に false になります。なぜなら、RuneError (U+FFFD) はサロゲートコードポイントの範囲外にあるためです。この冗長なチェックが、今回のコミットで修正されました。

新しいコードでは、switch ステートメントと fallthrough キーワードを巧みに利用して、この冗長性を排除しています。

switch i := uint32(r); {
case i <= rune1Max:
	p[0] = byte(r)
	return 1
case i <= rune2Max:
	p[0] = t2 | byte(r>>6)
	p[1] = tx | byte(r)&maskx
	return 2
case i > MaxRune, surrogateMin <= i && i <= surrogateMax: // エラーケースをまとめる
	r = RuneError
	fallthrough // RuneErrorは3バイト文字としてエンコードされるため、次のケースにフォールスルー
case i <= rune3Max: // ここでRuneErrorも処理される
	p[0] = t3 | byte(r>>12)
	p[1] = tx | byte(r>>6)&maskx
	p[2] = tx | byte(r)&maskx
	return 3
default: // 4バイト文字
	p[0] = t4 | byte(r>>18)
	p[1] = tx | byte(r>>12)&maskx
	p[2] = tx | byte(r>>6)&maskx
	p[3] = tx | byte(r)&maskx
	return 4
}

新しいロジックでは、i > MaxRune または surrogateMin <= i && i <= surrogateMax のいずれかの条件が満たされた場合、rRuneError に設定されます。その後、fallthrough キーワードによって次の case i <= rune3Max: に処理が移ります。RuneError (U+FFFD) は3バイト文字としてエンコードされるため、この case で適切に処理されます。これにより、RuneError に設定された後にサロゲートチェックを行うという冗長なステップが完全に排除されました。

t2, t3, t4 はUTF-8の先頭バイトのプレフィックス(例: 2バイト文字なら 110xxxxx110 部分)を定義する定数です。tx は後続バイトのプレフィックス 10xxxxxx10 部分、maskx は後続バイトのデータ部分を抽出するためのマスクです。

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

--- a/src/pkg/unicode/utf8/utf8.go
+++ b/src/pkg/unicode/utf8/utf8.go
@@ -329,37 +329,29 @@ func RuneLen(r rune) int {
 // It returns the number of bytes written.
 func EncodeRune(p []byte, r rune) int {
 	// Negative values are erroneous.  Making it unsigned addresses the problem.
-	if uint32(r) <= rune1Max {
+	switch i := uint32(r); {
+	case i <= rune1Max:
 		p[0] = byte(r)
 		return 1
-	}
-
-	if uint32(r) <= rune2Max {
+	case i <= rune2Max:
 		p[0] = t2 | byte(r>>6)
 		p[1] = tx | byte(r)&maskx
 		return 2
-	}
-
-	if uint32(r) > MaxRune {
+	case i > MaxRune, surrogateMin <= i && i <= surrogateMax:
 		r = RuneError
-	}
-
-	if surrogateMin <= r && r <= surrogateMax {
-		r = RuneError
-	}
-
-	if uint32(r) <= rune3Max {
+		fallthrough
+	case i <= rune3Max:
 		p[0] = t3 | byte(r>>12)
 		p[1] = tx | byte(r>>6)&maskx
 		p[2] = tx | byte(r)&maskx
 		return 3
+	default:
+		p[0] = t4 | byte(r>>18)
+		p[1] = tx | byte(r>>12)&maskx
+		p[2] = tx | byte(r>>6)&maskx
+		p[3] = tx | byte(r)&maskx
+		return 4
 	}
-
-	p[0] = t4 | byte(r>>18)
-	p[1] = tx | byte(r>>12)&maskx
-	p[2] = tx | byte(r>>6)&maskx
-	p[3] = tx | byte(r)&maskx
-	return 4
 }
 
 // RuneCount returns the number of runes in p.  Erroneous and short

コアとなるコードの解説

変更の中心は EncodeRune 関数の実装です。

変更前:

  • 複数の if および if-else if ブロックを使用して、rune の値に応じたエンコーディング処理を分岐していました。
  • 特に、MaxRune を超える値とサロゲートコードポイントの範囲内の値に対するエラー処理が2つの独立した if ブロックで行われていました。
    • if uint32(r) > MaxRuner = RuneError が行われる。
    • その直後に if surrogateMin <= r && r <= surrogateMax で再度 r = RuneError が行われる。 この2番目の if は、RuneError がサロゲートコードポイントではないため、最初の ifRuneError が設定された場合には常に false となり、冗長でした。

変更後:

  • switch ステートメントが導入され、rune の値 i (uint32にキャストされたもの) に基づいて処理が分岐されます。
  • case i > MaxRune, surrogateMin <= i && i <= surrogateMax: という単一の case で、無効な rune の条件(MaxRune を超える値、またはサロゲートコードポイント)がまとめられました。
  • この case がマッチした場合、rRuneError に設定されます。
  • 重要なのは、その後に fallthrough キーワードが使用されている点です。fallthrough は、現在の case の処理が完了した後、次の case の条件を評価せずに、その case のブロックに処理を移します。
  • RuneError (U+FFFD) は3バイト文字としてエンコードされるため、fallthrough によって処理は次の case i <= rune3Max: に移り、そこで適切に3バイトエンコーディングが行われます。
  • これにより、無効な rune のチェックと RuneError への変換、そしてその RuneError のエンコーディングが、より簡潔かつ効率的に行われるようになりました。
  • 最後の default: ケースは、残りのすべての rune (主に4バイト文字、つまり補助文字) を処理します。

この変更により、コードの行数が減少し、ロジックがより明確になり、冗長なチェックが排除されたことで、わずかながらパフォーマンスの向上も期待できます。

関連リンク

参考にした情報源リンク

  • Web検索結果: "Go unicode/utf8 package EncodeRune"
  • Web検索結果: "UTF-8 encoding Go rune"
  • Web検索結果: "Surrogate code points UTF-8"
  • Web検索結果: "Go RuneError"
  • Go言語の公式ドキュメント (pkg.go.dev)
  • GitHubのgolang/goリポジトリのソースコードI have generated the detailed explanation in Markdown format, including all the requested sections and information. I have used the commit data and the web search results to provide a comprehensive technical analysis. The output is to standard output only, as requested.