TT Lab
はじめる
学ぶ 学習パス コース

コンピュータ構成

メモリは一つではない

TT Labで続きを見る

目標

コンピューターの仕組みは、抽象的に学ぶと何も残りません。このPodの本物のCPUで、直接測ってみます。

ここで測る数字は、あとで「なぜこのコードは遅いのか」「なぜ計算が1ウォンずれるのか」といった疑問の答えになります。

見る場所

grep -m1 'model name' /proc/cpuinfo
cat /sys/devices/system/cpu/cpu0/cache/index0/size
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
nproc

時間を測るとき

Pythonは遅いです。そのため、読み取りの回数を固定して、配列のサイズだけを変えることで、Pythonの遅さが相殺され、メモリの違いだけが残ります。

また、ウォームアップを1回は必ず行ってください。最初の実行には、配列を作るコストが混ざります。

ステップ

  1. CPUとキャッシュ → 01-cpu.txt
  2. キャッシュ外のレイテンシ → 02-latency.txt
  3. キャッシュライン → 03-line.md
  4. 0.1 + 0.2 → 04-float.txt
  5. 足す順序 → 05-order.txt
  6. エンディアン → 06-endian.txt
  7. 2の補数 → 07-int.txt
  8. 整理 → 08-notes.md

参考

ステップ2の数字は、マシンごとに違います。絶対値ではなく倍率を見てください。キャッシュの内と外で2倍以上の差が出ていれば、正しく測れています。

このCPUとキャッシュを読み取る

このマシンのCPU名とキャッシュ階層(L1/L2/L3のサイズとラインサイズ)を取り出して、01-cpu.txtに残してください。

CPUはgrep -m1 'model name' /proc/cpuinfoです。キャッシュは、/sys/devices/system/cpu/cpu0/cache/index*/の下にあるlevel、type、size、coherency_line_sizeを読めばわかります。

キャッシュが階段のように何段にもなっていることと、レベルが上がるほど大きくなり遅くなることを、目で確認するステップです。

キャッシュの外に出たときの遅さを測る

読み取りの回数は同じにして、配列のサイズだけを変えながら、1回の読み取りにかかる時間を測り、02-latency.txtに残してください。4KB・1MB・64MBの3つで十分です。

核心は、読み取りの回数を固定することです。そうすれば、Python自体の遅さが相殺され、メモリの違いだけが残ります。

import array, random, time
def bench(elems, idx):
    a = array.array('i', [1]) * elems
    for i in idx[:1000]: a[i]        # 워밍업
    t = time.perf_counter()
    s = 0
    for i in idx: s += a[i]
    return (time.perf_counter() - t) / len(idx) * 1e9

ランダムなインデックスを30万個あらかじめ作り、3つの配列に同じ回数だけ使ってください。キャッシュの内と外で、2倍以上の差が出る必要があります。

順番に読むと速い理由を計算する

キャッシュラインのサイズを確認し、int32の配列なら1ラインに何個載るかを計算して、03-line.mdに書いてください。

ラインサイズはステップ1で見ました(たいてい64B)。int32は4バイトなので、1ラインに16個です。

メモリは、1バイトずつではなくライン丸ごと取得します。そのため、配列を順番に読むと、一度取得したラインから16個をタダで使えます。ランダムに読むと、毎回新しいラインを取得して、残りの15個は捨てます。

同じデータを扱うのに、アクセス順序を変えるだけで数倍速くなる理由がこれです。

0.1 + 0.2は0.3ではないことを確認する

0.1 + 0.2の結果と== 0.3の判定、そして0.1を小数点以下20桁まで出力した値を、04-float.txtに残してください。

print(f"{0.1:.20f}")です。0.1は2進数でぴったり表せません。10進数で1/3を書き切れないのと同じです。

そのため、お金をfloatで扱ってはいけません。整数(ウォン単位)か十進型を使います。

足す順序が結果を変えることを示す

同じ3つの数を、順序だけを変えて足したときに結果が変わる例を作り、05-order.txtに残してください。

1e16、1.0、-1e16を使ってみてください。

1e16 + 1 - 1e16 = 0.0
1e16 - 1e16 + 1 = 1.0

大きな数に小さな数を足すと、小さいほうが桁の外に押し出されて消えます。そのため、浮動小数点の足し算は結合法則が成り立たず、合計を求めるときは小さいものから足すほうが正確です。

同じ数字を別のバイト順で表す

0x12345678を、リトルエンディアンとビッグエンディアンでそれぞれ4バイトにして、06-endian.txtに残してください。このマシンがどちらかも一緒に書いてください。

struct.pack('<I', n).hex()とstruct.pack('>I', n).hex()、そしてsys.byteorderを使います。

リトルエンディアンは78563412と、ひっくり返って見えます。ファイルやネットワークで数値をやり取りするときにこれを揃えないと、値がおかしくなります。ネットワークバイトオーダーがビッグエンディアンと決められている理由です。

2の補数とオーバーフローを確認する

2**31を符号付き32ビットとして読むと何になるか、-1を符号なし32ビットとして読むと何になるかを確認して、07-int.txtに残してください。

struct.unpack('<i', struct.pack('<I', 2**31))[0]と、その逆を試します。

2**31 → -2147483648、-1 → 4294967295が出ます。同じビットをどう読むと決めたかの違いにすぎません。

他の言語で、整数が突然負の数になる事故(オーバーフロー)の正体がこれです。Pythonの整数は自動的に大きくなりますが、それはPythonが特別なのです。

3つのことを整理する

08-notes.mdに3行以上書いてください。キャッシュの外へのアクセスがなぜ遅いのか、お金をfloatで扱ってはいけない理由、バイト順を揃えなければならないときです。

本文に캐시、소수、순서が含まれている必要があります(韓国語で、順に「キャッシュ」「小数」「順序」を意味する語です)。