自己表現しつつ英語を勉強 Express Myself And Study English

ゲーム、プログラム技術、スッタニパータについて日本語と英語で書いていこうというブログ About games, programming, and Buddhism written in English and Japanese

3900Xの低レイテンシの秘密はタイムスタンプベースのキャッシュコヒーレントプロトコルではないか/3900X may implement a timestamp-based cache coherence protocol

https://pbs.twimg.com/media/D-4H6TDUYAEUe_7.jpg (https://twitter.com/0x22h/status/1147863835161722881)
https://i.redd.it/siy01dt9c3931.png (https://www.reddit.com/r/Amd/comments/calue1/intercore_data_latency/)

If those pictures are true, I think Zen 2 implements a timestamp-based cache coherence protocol.

http://arc.zju.edu.cn/_upload/article/files/2a/ea/d86bceee4adbaf63645ba719d66e/df7d122f-28f8-49ee-9bd3-5795967e9847.pdf

AMDのZenアーキテクチャは4コアをひとまとめにしたCCXを複数組み合わせて多コアのシステムを構成するわけですが、CCX内に含まれる4コアの中でデータをやり取りする場合は速いけれど、CCX外への通信は遅くなる、という弱点がありました。その弱点はZen2で完全に払拭されたようです。

https://www.tomshardware.co.uk/amd-ryzen-threadripper-1950x-cpu,review-33976-2.html

この記事によると、CCX内のコア同士は40-80nsぐらいで通信できますが、CCXをまたぐと平均120ns、複数のCPUダイを組み合わせたThreadripperの場合、ダイをまたぐことで平均238nsの遅れが生じます。このレイテンシは特にゲームのような複雑なアプリケーションの性能を低下させます。


それがZen 2の場合、冒頭の画像にあるように、CCXをまたいでも、ダイをまたいでも70nsぐらいのレイテンシで通信が出来ています。これが本当だとすれば、とてつもない進化です。

これをどう実現しているかですが、まずひとつのCPUダイ内に2つのCCXがあるわけですが、その同一ダイ内のCCX同士が通信するときにも、まずIOダイと呼ばれるメモリ等へのアクセスを担当するダイと通信します。そして、IOダイから再びCPUダイにデータを送ることで、通信をしているということです。

https://twitter.com/Thracks/status/1148034322785591298

Threadripperでは、一つとなりのダイと通信するだけで、同一ダイでの通信の倍ぐらいのレイテンシが発生したのですが、Zen 2では同じダイ内のCCXと通信するために、2つのダイをまたいでいます。それでもなお、Zen 1の同じダイ内での通信よりも小さいレイテンシで通信ができています。これはものすごく異常なことです。

さらにいえば、Zen 2ではL3キャッシュが倍増しているので、管理が大変になり、レイテンシも本来増えるはずです。レイテンシがここまで減少したからには、キャッシュ管理の革命的な単純化がなされたのだろうと思われます。

ここでキャッシュ管理について説明しようと思います。CPUはメモリからデータを読みこむ時、かなり時間がかかります。その時間がもったいないので、一度メモリから読んだデータはキャッシュにとっておき、次に読み込むときはメモリまで読みに行かず、キャッシュから読み込むことですばやくアクセスします。

昨今のCPUでは複数のコアがあるのが当たり前で、複数のコアがそれぞれ同時に計算を行います。2つのコアが同時に計算することで、二倍の速度で計算が終わる事もありえます。

コアそれぞれがキャッシュを持っており、それぞれの計算に使います。問題は、同じメモリのデータを複数のコアがキャッシュしている場合です。

CPUはメモリを書き換えるのは大変なので、まずキャッシュの中のデータを書き換え、メモリの中のデータはできるだけ書き換えないようにするのが基本です。

あるコアがデータを0から1に書き換えようとします。そのためにはまずそのコア内のキャッシュを書き換えることになります。もしそのデータを他のコアもキャッシュしていたら、書き換えたコアでは1だけれど、他のコアでは0のままという、コアによって違うデータが生じてしまうことになります。

これを認めるとプログラムが誤作動してしまいますので、こういうことが起こらないようにするためにCPUは絶大な苦労をしています。この仕組みをキャッシュコヒーレンシプロトコルといいますが、これがキャッシュアクセスに大きなレイテンシが発生する主な原因の一つにもなっています。

具体的に方法を述べると、複数のコアで共有されたデータを書き換える時、まず書き換える前に、そのデータを共有しているすべてのコアに対して「キャッシュを無効にしろ」と命令します。すべてのキャッシュでそのデータが無効になったら、データを書き換えます。その後書き換えられたデータは、必要なら各コアで取得しなおされます。これによってデータの一貫性が保たれます。

しかし、まずデータをどのコアがキャッシュしているのかを常に把握しておくのが大変ですし、その全てに対して命令を送るのも、その命令が無事遂行されたのを確認するのも大変です。Threadripperのように別ダイにまでデータが拡散していた場合は、そこまで無効化信号を送るのはさらに大変になります。

根本的に、N個のコアがN個のコアに対して無効化信号を送りうるので、Nの2乗の通信が発生し、そのレイテンシで全コアが停滞することにもなりかねません。そしてその影響は、コア数が増えるほどに大きくなっていきます。

はっきり言えば、この旧来の仕組みでまともに動かせるのは、Intelの8コアぐらいまでだと思います。Intelの採用しているリングバスは、コア間で通信する時に必ず全コアを通過するので、全コアに対して無効化信号を送ることが簡単に出来ます。データをどのコアが持っているか追跡する必要もありません。しかし、どこと通信するにも必ず全コアを通過しなければならないので、コア数がふえるほどに通過するコアが多くなり、レイテンシが増えていきます。なので、8コアぐらいが効率的に動かせる限度だろうと思います。

Zen 1ではMDOEFSIというおそろしく複雑なキャッシュシステムが搭載されていて、その結果隣のCCXのキャッシュにアクセスするのにメインメモリへのアクセスより時間がかかるという事態に陥っていました。これがどう解消されたのかということですが、私の予想は「タイムスタンプベースのキャッシュコヒーレントプロトコル」です。

つまり、複数のコアがデータを共有するときには、メインでデータを持っている一つのキャッシュ以外は期限付きでデータを借ります。期限が来たらそのデータは勝手に無効化されるので、データを使い続けたい場合は再び借り直しに行かなければいけません。

これの何がうれしいかというと、データを書き換える時に、まず貸出を停止し、すべてのコアで期限が来て無効化されるのを待ってからデータを書き換えることで、キャッシュの一貫性を簡単に保てることです。

本来、プログラマがまともならば、各コアで共有され、かつ書き換えられるデータはごくわずかにとどまり、大半は共有されるけれども、読むだけで書き換えられないデータになります。ただ、そのごくわずかの、たとえば並列キューの排他フラグのようなものは、ものすごく頻繁に書き換わり、いわば各コアによる書き換え権の奪い合いのような状態にもなりやすいです。なので、よく書き換えられるデータは期限を短くし、その他大半の「読まれるだけの共有データ」に関しては期限を長く設定することで、簡単に効率を高めることが出来るはずです。

旧来の仕組みでは、ごく僅かにしかおこらない共有メモリの書き換えのために全共有メモリをトラッキングするような恐ろしい無駄が発生していたのが、だいぶ簡単になるはずです。それがZen 2の秘密ではないかと推測した次第であります(まあそもそも2ダイをまたぐ通信のレイテンシが小さいというソース自体が心もとないので、推測の推測のようなものですが・・・)