ChatGPTを活用してGo Proposalを読み解いてみた
目次[非表示]
- 1.はじめに
- 2.Go Proposalとは?
- 3.おわりに
はじめに
こんにちは、divxエンジニアの阪村です。
普段の業務ではGoを扱うことが多いです。
今回は、Go Proposalについての紹介とChatGPTを用いたテクニカルな難しい内容の記事を読み解く方法の一例をご紹介します。
Go Proposalとは?
Go Proposalとは、その名の通り、Goの新機能や既存機能、標準ライブラリの修正・改善提案のことです。
Goの開発チームにとっては、開発を公平で透明性のあるものにし、Goのコミュニティから広くフィードバックを収集するために活用してされています。
基本的には、提案に対して、文書化→レビュー→承認→実装といったプロセスを経て新しいバージョンの機能としてリリースされます。詳しい開発の流れに興味がある方はこちらをご確認ください。
この変更提案はgitthub上でも公開されており、誰でも確認・提案ができます。
Go Proposalを確認することで、以下の様なメリットが得られます。
Goの最新動向を抑えることができる
Goに今後どのような新機能が追加されそうか、または既存仕様にどのような変更が予定されているのかを事前に把握ができます。現在の自分が担当しているプロジェクトへの影響を考慮しておくことができます。
プログラミング全般に関する理解が得られる
挙げられている提案を新たな提案を読むことで、Goだけに限らず最先端のソフトウェア開発の手法やトレンドを学ぶことができます。また、提案の議論からは、複雑な技術的課題の解決方法や、それらに対する他のエンジニアの意見を学ぶこともできます。
オープンソース開発に貢献できる
見るだけでなく自分から提案をすることや挙げられている提案に対しての議論に参加することもできます。
上記でも挙げているように、国内外のエンジニアがテクニカルな内容の議論を繰り広げており、内容を確認するだけでもエンジニアとして勉強になります。
ただ、時には自分の前提知識が足りていなかったり、難しすぎたりして議論についていけないこともあります。
しかし、いまはAIを気軽に活用できる時代です。AIを副操縦士として活用して理解の手助けをしてもらいます。知識を効率良く取り込みたいと思います。
今回題材にする記事はこちらの過去の記事です。(リンクはgoogleが公開しているものですが、githubにて公開されているものと内容は同じです。)
https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md
内容は以降で見ていきますが、Goのtimeパッケージに関する記事になります。
ざっくり要約してもらった
文章が長いので、ChatGPTでは文字数制限にかかるにかかってしまいます。そこで今回は、Grarityというgoogle chromeプラグインを利用して、全体を読み込ませ、ざっくりと概要を把握しました。
Grarityは内部でChatGPTを利用しており簡単な操作でWebページや検索内容、YouTube動画内容を要約、翻訳してくれます。
Summary
概要:この記事では、Go言語における時間の比較と減算の問題について説明されています。time.Nowによって観測された時刻の比較や減算は、システムの壁時計が2つの観測の間にリセットされた場合、正しくない結果を返す可能性があります。この問題を解決するために、time.Timeの表現を拡張して、これらの計算に使用するための追加の単調時計の読み取りを保持することが提案されています。この変更により、time.Nowとtime.Sinceを使用した基本的な経過時間の計測が負の期間や現実に基づかない他の結果を報告することは不可能になるはずです。
要点:Go言語のtime.Nowによる時間の比較や減算は、システムの壁時計のリセットによって誤った結果を返す可能性がある。この問題を解決するために、time.Timeの表現を拡張し、追加の単調時計の読み取りを保持することが提案されている。これにより、経過時間の計測において負の期間や現実に基づかない結果が報告されることはなくなる。
Summary(要約)だけでなくImportant(重要箇所)も作成してくれます。
Important
time.Nowによって観測される時間の比較と減算は、システムの壁時計が2つの観測の間にリセットされた場合、正確な結果を返さない可能性がある。
time.Timeの表現を拡張して、これらの計算に使用する追加の単調なクロックの読み取りを保持することを提案している。
単調なクロックを使用することで、time.Nowとtime.Sinceを使用した基本的な経過時間の計測が、負の期間や現実に根差さない他の結果を報告することが不可能になる。
コンピュータの時計には高精度なクロックがあり、そのクロックのティックを秒の等価数に変換する方法がある。
コンピュータの時計はリセットされることがあり、また閏秒を無視することが多い。
コンピュータの時計では、壁時計と単調なクロックの2つの異なるクロックにアクセスできるようになっている。壁時計は時間を示すために使用され、単調なクロックは時間を測定するために使用される。
Goの現在の時刻APIは、システムの壁時計を読み取り、単調なクロックを読み取らないため、時刻の測定が正しくない場合がある。
GoのAPIのいくつかは、単調なクロックを正しく使用するべきであるが、互換性のために現在の実装が使用されている。
測定経過時間、ネットワーク接続のタイムアウト、およびコンテキストの期限など、いくつかの標準のGo APIが問題の対象となっている。
Goは、経過時間を含む計算をサポートし、デッドラインのチェックを改善する必要がある。
※GoとGo言語と表記揺れがありますが、出力された内容をそのまま載せています。
翻訳と要約を同時にやってくれたので、なんとなく全体像が把握できました。
一部「壁時計」と「単調な時計」という不自然な単語があったので原文を確認してみます。それぞれwall clock と monotonic clock と表されていました。
これらは、Goに限った概念ではありませんので、知ってる人であればスッと理解できると思います。しかし、知らない人にとっては内容を理解するための1つのハードルとなってしまいます。
こんなときこそ、ChatGPTの出番です。
ChatGPTに質問してみました。
Question
wall clock と monotonic clockの違いはなんですか?
Answer
「wall clock」はリアルタイムを計測するための物理的なクロックです。一方、「monotonic clock」はコンピュータの内部で使用される論理的なクロックで、一貫して進行する時間を提供します。
詳しく説明すると、"wall clock"は現実世界の時間を追跡し、日付と時刻を表示するのが一般的です。この時間は、時間帯の変更、夏時間への調整、または時計を手動で調整することで変わる可能性があります。
一方、"monotonic clock"は、システムのアップタイム(起動してからの経過時間)を計測します。この時間は一定の速度で進み、一度設定されると後退することはありません。ただし、これは物理的な時間とは必ずしも一致しないため、経過時間の測定にのみ使用されます。例えば、プログラミングにおいては、タスクの実行時間を計測するために使われることがあります。
したがって、これらは時間を測定する目的と使用方法が異なるため、異なる状況で使用されます。
「わかったような、わからないような…」状態です。もっと少し内容を理解するために追加の質問をしました。
Question
小学生でもわかるように例を用いて説明してください。
Answer
もちろんです、丁寧に説明しますね。 「Wall clock」は壁掛け時計のようなものです。つまり、あなたが毎日家で見ている時計です。「今何時?」と聞かれた時に答えてくれる時間です。でも、この時間は、夏時間にする時に時計を1時間進めたり、違う国や都市に旅行に行った時に時差で変わったりします。
一方、「Monotonic clock」は、もしあなたがゲームを始めてそのプレイ時間を計るストップウォッチやタイマーのようなものだと考えてみてください。ゲームを始めてから終わるまでの時間を正確に教えてくれます。でも、その間にどれだけ日が暮れたか、または時計の時間がどれだけ変わったかは関係ありません。つまり、ストップウォッチは自分の時間をただ一貫して進めていくだけです。
だから、「Wall clock」は「今何時?」と知りたい時に使うもので、「Monotonic clock」は「これはどれだけ時間がかかった?」と知りたい時に使うもの、と覚えておくと良いですね。
「小学生でも」と文章を加えたからか、口調も優しくなった気がします。個人的には、普通の時計とストップウォッチの例えが個人的にはわかりやすく、しっくりきました。
これらを踏まえて、Glarityが作成してくれた要点をChatGPTに改めて整理してもらいます。
Question
以下の文章内で'壁時計'は'Wall clock'、'単調時計'は'Monotonic clock'のことを表しています。これを踏まえて文章をわかりやすい表現に変えてください。
※省略(先程のGrarityが作成したsummaryとimportant)
Answer
概要:この記事では、「Go」というプログラミング言語で時間を扱う時の問題を解説しています。この言語で時間を取得する機能(time.Now)を使って時間を比較したり、時間を計算すると、コンピュータの時間がリセットされるときには、間違った結果が出てしまう可能性があります。これを解決するために、時間を表示する機能(time.Time)を詳しく表示し、さらにストップウォッチのような時間計算機能も追加した方がいいという提案が出されています。この改善により、計算した時間が実際とは合わなかったり、マイナスになったりするという問題は起きなくなるはずです。
要点:「Go言語で時間を取得するtime.Nowという機能には、コンピュータのシステム時間がリセットされると、正しくない結果が出てしまう可能性があります。これを解決するために、新しいアイデアが出ています。それは、time.Timeという時間を示す機能をより詳細に表示し、さらに「ストップウォッチ」のように動作する時間機能も追加しようというものです。これができれば、計測した時間がマイナスになったり、実際の時間と全く合わない結果が出るという問題は解消されます。」
なるほど、このときのGoのtime.Timeパッケージには、ストップウォッチ形式で経過時間を取得する方法がなかっため、結果が意図せぬ値になってしまうことがあったので、それに対応するための修正・実装の提案といった内容だということが理解できました。
※モノトニッククロックはGo1.9から正式にtimeパッケージにて採用されるようになっています。
ざっくりと内容を抑えて、Goの今後のバージョンアップ方針を掴むだけならこれだけで十分そうです。
提案内容の実装内容を確認
すでに承認やレビューがされている過去のGo Proposalには、その提案をどうやって実装する(した)のかが書かれています。これを確認することは、オープンソースコミッターの考え方とコードを学ぶことに繋がり、エンジニアとして有用だと考えます。今回はこの部分もChatGPTに活躍してもらいながら掘り下げていきます。
実装方法について以下のように提案がされています。
提案
後方互換性とAPIをシンプルにするために、timeパッケージにモノトニッククロックのアイデアを公開する新しいAPIを導入しないことを提案する。
その代わりに、time.Timeをウォールクロックの読み取り値と、オプションで追加できるモノトニッククロックの読み取り値の両方を格納するように変更すること、time.Nowを両方の時計を読み取り、両方の読み取り値を含むtime.Timeを返すように変更すること、t.Add(d)を、両方の読み取り値(存在する場合)がdで調整されたtime.Timeを返すように変更すること、t.Sub(u)を、tとuの両方がモノトニッククロックの時刻を持つ場合に操作するように変更することを提案する。このようにすることで、開発者は常にtime.Nowを使い続けることができ、実装は「時間を知るにはウォールクロックを使い、時間を測るにはモノトニッククロック」を使うというルールに従うことができる。
具体的には、timeパッケージのドキュメントを変更し、実装を変更することを提案します。
※中略
この提案では、モノトニッククロックは開発者が学ぶべき新しい概念としてではなく、既存のAPIを使った時間計測の精度を向上させる実装の詳細として扱います。開発者は何も新しいことを学ぶ必要はなく、明らかなコードがただ動くだけである。実装はルールを適用し、開発者はそれについて考える必要はない。
※DeepLにて翻訳
timeパッケージに新しいパッケージを公開せずに代わりにtime.Time型のフィールドとtime.Nowメソッドで返す値を変更することが提案されています。そうすることで既存のtime.Nowを使用しているプロジェクトのコードを変更する必要がなくなるとのことです。後方互換性を意識した開発者に優しい設計ですね。
肝心の変更点の部分は以下の様に紹介されています。
現在(このGo Proposalが書かれた時点)のtime.Timeの定義は以下の通り: type Time struct { sec int64 // seconds since Jan 1, year 1 00:00:00 UTC nsec int32 // nanoseconds, in [0, 999999999] loc *Location // location, for minute, hour, month, day, year }
オプションで単調時計の読み取りを追加するには、次のように表現を変えればよい: type Time struct {
wall uint64 // wall time: 1-bit flag, 33-bit sec since 1885, 30-bit nsec
ext int64 // extended time information
loc *Location // location
}
wallフィールドは、33ビットの秒と30ビットのn秒にパックされた経過時間をエンコードすることができる(別々にすることで、コストのかかる分割を避けることができる)。
2の33乗秒は272年なので、wallフィールドはそれだけで1885年から2157年までの時間をナノ秒精度でエンコードできる。
t.wallの先頭フラグビットがセットされている場合、wall秒は前述のようにt.wallにパックされ、t.extはGoプロセス起動からのナノ秒として格納されたモノトニッククロックの読み取り値を保持する(プロセス起動に変換することで、オペレーティングシステムが64ビットより大きな表現を返した場合でも、モノトニッククロックの読み取り値を確実に格納できる)。
そうでない場合(先頭のフラグビットがクリアされている場合)、t.wallの33ビットフィールドはゼロでなければならず、t.extは元のTime表現と同様に、1年1月1日からの64ビット秒を保持する。ゼロのTimeの意味は変わらないことに注意。このことは、モノトニッククロックの読みは、1885年から2157年までのウォールクロックの読みと一緒に保存することができるということを意味している。
time.Nowの結果とその近辺の時刻にモノトニッククロックの読み取り値を格納する必要があるだけであり、それらの時刻は1885年から2157年の範囲内に収まることが期待される。
この一般的なケースでは、ウォールクロックの読みと一緒にモノトニッククロックの読みも格納できなければならない。
Unixベースのシステムでは1970が使われることが多く、Windowsベースのシステムでは1980が使われることが多い。しかし、NTPプロトコルのエポックは1900を使用しているため、1900より前の年を選択した方が将来性があると思われる。
64ビットシステムでは、現在の表現ではnsecとlocの間に32ビットのパディングギャップがあるが、新しい表現ではこれを埋めるため、構造体全体のサイズは24バイトに保たれる。32ビットシステムでは、このようなギャップはなく、構造体全体のサイズは16バイトから20バイトに増加する。
話が専門的になってきて、ついていけなくなりました。ChatGPTの出番です。
まず、ざっくりと聞いて、そこから具体的にわからない部分を解説してもらいます。
Question
以下のGoのtimeパッケージに関する説明がよく理解できません。小学生でもわかるように解説してください。
※省略(先程の文章)
Answer
Go プログラミング言語の 'time' パッケージは、コンピュータのメモリを大量に消費しないように、特殊な方法で時間を保存します。
これをパズルの収納方法に例えてみましょう。パズルを広げたままにはせず、精巧に箱に収納します。ここでは、外側のピースと中央のピースを分けるなどします。
同様に、Goのtimeパッケージは時間を二つのパーツ、通常の時間(ウォール時間の時間と呼びます)と モノトニック'時間(Goプログラムが開始されてからの時間)に分けて保存します。
ウォールクロックの時間は秒と秒の一部(ナノ秒)に分けられ、それぞれ33ビット部分と30ビット部分に保存されます。これは非常に大きな数値を、兆や億などに分けて保存するのに似ています。この方法で、高い精度(ナノ秒レベル)で多くの年数を表現することができます。
このパズルボックスには、モノトニック時間を保存するための特別なスペースもあります。これを使用する必要がある場合は、シグナルが送信され、「モノトニック時間を保存しています!」と告げます。このモノトニック時間は、Goプログラムが開始してからのナノ秒単位の時間として保存されます。これはプログラムが実行されたときにカウントを開始したストップウォッチのようなものです。
ただし、1つ注意点があります。最高レベルの詳細度で、1885年から2157年の間の通常の時間のみ、このストップウォッチ時間を保存することができます。ほとんどの場合、これは問題ではありません。なぜなら、このモノトニック時間は現在起こっている事象のみに必要であり、我々は現在がこれらの年数の間にあることを確信しているからです。
最後に、このパッキング方法は、異なるコンピュータシステムでtimeパッケージのサイズが同じ、またはほぼ同じになることを保証します。これは、棚に置かれたときに自身を並べ替えてフィットする魔法のパズルボックスのようなものです。
Question
なぜ、33ビットと30ビットの部分に保存するのですか?
Answer
それは時間の表現に必要なビット数を最小限に抑えるためです。一般的にコンピュータ内部で時間は秒とナノ秒で表現されますが、この両者を一緒に1つの64ビットの数字として保存すると不要な時間が生じます。
そこで、Goのtimeパッケージでは壁掛け時計の時間を秒単位で33ビットに、その秒の一部(ナノ秒)を30ビットに分けて保存します。
それぞれ33ビットと30ビットに分けることで、秒とナノ秒の時間を十分に表現でき、かつ余分なスペースを使わない効率的な保存が可能になります。特に、33ビットを秒単位で利用すると約272年分の時間を表現でき、これは1885年から2157年までの時間を表現するのに十分な範囲です。
これは、Goのtimeパッケージが現代と近未来の時間を扱うことを想定しているためです。対して、30ビットのナノ秒部分は1秒を10億(1ナノ秒は10億分の1秒)に分けるのに十分で、これにより時間をナノ秒単位で正確に表現できます。
Question
33ビットで表現できる秒数は272年になるのですか?
Answer
はい。33ビットが表現できる状態の数は2の33乗で、これは約86億です。ここでは、1の状態を1秒とみなしています。
なお、1年は約31536000秒(60秒×60分×24時間×365日)なので、33ビットで表現できる秒数を年数に換算すると約272年(= 86億秒 ÷ 31536000秒/年)になるわけです。
ここまでのChatGPTとのやり取りで私は内容を以下の様に理解しました。
これまでのTime型のsecとnsecのフィールドを統合して、wallフィールドとして、ウォールクロックを管理する。
Time型に新たにextフィールドを追加する。このフィールドで、モノトニッククロックの時間(ストップウォッチの様な経過時間)を管理する。
これにより以前のTime型はウォールクロックとモノトニッククロックの両方に対応ができるようになる。ただし、モノトニッククロックが機能するのは1885年から2157年の日時の場合のみである。
ChatGPTにも自分の理解があってそうか確認してみました。
Question
要約すると以下の内容であるという認識であってますか?
※省略(上記の理解した内容を箇条書きにしたもの)
Answer
はい、その通りです。要するに、これまではsecとnsecフィールドがそれぞれ秒とナノ秒を管理していましたが、wallフィールドに統合することで、ウォールクロック(実際の経過時間)を管理し、新たに追加されるextフィールドでモノトニッククロック(経過時間のみを示す一定方向の時間)の時間を管理するように変更するという提案内容です。これにより、構造体Timeはウォールクロックとモノトニッククロックの両方を扱うことが可能となり、より広範囲の時間表現が可能になります。
ChatGPT先生にもお墨付きをいただけました。
この変更は後方互換性のためにtime.Now関数にて、モノトニッククロックもウォールクロックも開発者が意識せずに使えるということでした。
実際にNow関数でどのように実装されているか見てみましょう。
このGo proposalの記事内には具体的なコードの変更はありませんでしたので、2023年7月現在のバージョン1.20のGoのコードで確認しました。
// Now returns the current local time.
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
// Seconds field overflowed the 33 bits available when
// storing a monotonic time. This will be true after
// March 16, 2157.
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
プラベートの定数が用いられているので、このコードだけでは詳細はわかりません。
ただ、命名からそのまま察すると最後のreturnの部分がやや複雑にはなっているものの、提案されていた通りにNow関数でウォールクロックとモノトニッククロックを値の両方を取得できるようになっていることが確認できました。
詳しく他の部分のコードを見てみてもいいですが、せっかくなので今回はChatGPTに解説してもらいコードの詳細を確認しましょう。
Question
以下のGoで書かれたコードの解説をしてください。
※省略(Now関数のコード)
Answer
この関数は、現在のローカル時間を生成して返すGo言語のコードです。
1行目のコメントにもあるように、この関数の目的は現在のローカル時間(ローカルタイムゾーンでの現在時間)を返すことです。
関数Now()はTimeという形式の値を返します。TimeはGo言語の標準パッケージtimeで定義されている構造体で、時間を表現するためのものです。
sec, nsec, mono := now()でnow()という関数を呼び出し、返り値をそれぞれの変数に格納しています。now()は恐らく現在の時刻をナノ秒単位で返す関数で、secは秒、nsecはナノ秒、monoはUNIX時間からの経過時間を表しているものと思われます。
次にmono -= startNanoで開始時間を引き、sec += unixToInternal - minWallでバイアスを修正しています。
次に、秒が33ビットを超えているかどうかのチェックが行われます。これは、現在の経過時間が33ビットを超えてしまうと、時間の表現がオーバーフローしてしまうためです。このチェックが真になるのは2157年3月16日以降とコメントに書いてあります。
それ以降の処理は、時間の値をTime構造体に設定して返しているだけです。ここでもhasMonotonic | uint64(sec)<<nsecShift | uint64(nsec)というビット演算が行われていますが、これは内部的に時間をどのように表現しているかに依存する部分です。
Question
変数monoにはどのような値が入りますか?
Answer
変数monoには現在のモノトニック時間(つまり、一定の速度で進む時間)が代入されます。具体的には、システムが起動してからの経過時間をナノ秒単位で表したものが格納されています。
ここで言うモノトニック時間とは、一般的にシステム時刻(現在時間)と区別される時間の一種で、システムが起動してからの経過時間を指します。この時間は、システムの現在時刻が変更されたり、デイライトセービングタイムのような時間調整があったとしても影響を受けません。
このような時間を用いるのは、実時間と比較してタスクの実行時間を測定する際などに有用で、時計のずれや他の要因による影響を受けずに一定の時間間隔を保証することができます。具体的な用途としては、ベンチマークテストの実行時間の測定や、経過時間の計測などがあります。
ChatGPTを使えば、手軽にどんどん深ぼっていけますね。いったん、当初の目的であるGo Proposalでされている提案内容の確認とそれをどのように実装するのかの把握というは達成できましたので、当記事ではここまでとしたいと思います。
おわりに
Go Proposalの紹介と難しい内容の記事の内容を理解するためのChatGPTの活用例を紹介させていただきました。
AIを活用することで今まで理解が難しかったことや分かった気になっていたことにアプローチしてエンジニアとしての力を伸ばしていくことができます。コードを書いてもらうことだけがエンジニアにとってのAI活用法ではありません。
※ちなみに、今回は趣旨とは異なるためChatGPTプロンプトのベストプラクティスはそこまで意識せずにやり取りをしました。
精度などを気にされる方は、ペルソナを設定したり、ステップに分けたりといったベストプラクティスに則ってプロンプトを作成するとより良い回答が得られるかもしれません。
DIVXでは一緒に働ける仲間を募集しています。興味があるかたはぜひ採用ページを御覧ください。