"""
=============================================================================
CS349E · 前置讲 P1 · 01_how_a_cpu_runs.py —— 处理器怎样执行程序：指令、时钟、流水线、依赖
=============================================================================

【这个脚本要回答的问题】
    "一段代码为什么要这么久？"在 roofline 之前，先要知道处理器是一条一条执行**指令**的。
    本脚本用四个演示把这件事讲清楚：

      演示 1：一行 Python 的点积，在机器眼里是哪些指令？性能方程：时间 = 指令数 × CPI ÷ 频率。
      演示 2：流水线——为什么它不缩短单条指令的延迟，却能把吞吐提高好几倍；停顿（气泡）的代价。
      演示 3：依赖链——s += x[i] 每次都要等上一次的结果；用多个累加器打破依赖（指令级并行 ILP）。
              再用 numpy 实测：没有依赖的 sum 和有依赖的 cumsum，每个元素差多少。
      演示 4：解释器的开销——纯 Python 循环和 numpy 的点积，每个元素差几百倍。

【怎么读输出】演示 1–2 和演示 3 的模型部分是公式，每次相同；演示 3 的实测部分和演示 4 是墙钟测量，
    取决于你的机器，只看数量级和趋势（编写时用的是 Apple M4 Pro）。

【运行】python3 01_how_a_cpu_runs.py           （不到 1 秒）
        python3 01_how_a_cpu_runs.py --quick   （更快，只检查能不能跑通）
【依赖】numpy；从 ../lecture01 导入 console（表格打印）。
"""

import os
import sys

import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))
# 【Python 小知识】sys.path 是 Python 查找模块的目录列表。把 ../lecture01 加进去，
#   就能 import 第 1 讲写好的 console.py，不用复制一份。

from console import print_header, print_observations, print_table  # noqa: E402
from arch_lab import pipeline_cycles, reduction_throughput, best_time  # noqa: E402

QUICK = "--quick" in sys.argv


# ---------------------------------------------------------------------------
# 演示 1：从代码到指令
# ---------------------------------------------------------------------------
def demo1_instructions(freq_ghz=4.0):
    """
    把 s += a[i] * b[i] 的循环写成"伪汇编"，数一数每个元素要几条指令，再用性能方程估时间。

    【性能方程（Iron Law）】
        时间 = 指令条数 × 每条指令的平均周期数（CPI） ÷ 时钟频率
        三项分别由"算法和编译器"、"微体系结构"、"工艺和电路"决定。
    【参数】freq_ghz：假设的时钟频率（GHz）
    【简化】伪汇编是示意，不对应任何真实指令集；真实编译器会展开循环、用 SIMD 指令（演示 3、P1 §6）。
    """
    print_header("演示 1：一行点积在机器眼里是什么？")
    asm = [
        ("loop:", ""),
        ("  load   f1, [a + i*4]", "从内存读 a[i] 到寄存器 f1"),
        ("  load   f2, [b + i*4]", "读 b[i]"),
        ("  fma    f0, f1, f2, f0", "f0 = f1 × f2 + f0（乘加，一条指令算 2 次浮点运算）"),
        ("  add    i, i, 1", "下标加 1"),
        ("  cmp    i, n", "比较 i 和 n"),
        ("  blt    loop", "如果 i < n 就跳回 loop（分支）"),
    ]
    for ins, note in asm:
        print(f"    {ins:<26}{('; ' + note) if note else ''}")
    n_instr = 6
    rows = []
    for cpi in (1.0, 0.5, 0.25):
        t_ns = n_instr * cpi / freq_ghz
        rows.append([f"{cpi}", f"{1 / cpi:.0f}", f"{t_ns:.2f} ns", f"{2 / t_ns:.2f} GFLOP/s"])
    print()
    print_table(["CPI（每条指令几个周期）", "IPC（每周期几条指令）", f"每个元素的时间（{freq_ghz} GHz）", "单核算力"], rows)
    print_observations([
        "每个元素 6 条指令，其中真正'做运算'的只有 1 条 fma；其余是搬数据（load）和管理循环（add、cmp、blt）。",
        "现代 CPU 每个周期能发射好几条指令（IPC > 1，CPI < 1），这叫'超标量'（superscalar）。",
        "即使 CPI = 0.25，单核标量代码也只有几 GFLOP/s。要更快只有两条路：一条指令处理多个数据（SIMD），"
        "或者用更多的核——P1 §6 和 P2 会讲。",
    ])


# ---------------------------------------------------------------------------
# 演示 2：流水线
# ---------------------------------------------------------------------------
def demo2_pipeline():
    """
    比较不用流水线和 5 级流水线执行 n 条指令的周期数，以及停顿（stall）的影响。

    【5 级】取指（IF）→ 译码（ID）→ 执行（EX）→ 访存（MEM）→ 写回（WB）。
    【类比】洗衣服：洗、烘、叠三步。一个人洗完烘完叠完再洗下一批，3 批要 9 个时段；
        流水作业时第二批在第一批烘的时候就开始洗，3 批只要 5 个时段。每一批的"延迟"没变，"吞吐"提高了。
    """
    print_header("演示 2：流水线——延迟不变，吞吐提高")
    stages = 5
    rows = []
    for n in (1, 5, 100, 10000):
        no_pipe, pipe = pipeline_cycles(n, stages)
        rows.append([n, no_pipe, pipe, f"{no_pipe / pipe:.2f}×", f"{n / pipe:.3f}"])
    print_table(["指令条数", "不用流水线（周期）", "5 级流水线（周期）", "加速比", "每周期完成几条"], rows)

    print("\n停顿（stall）的代价：10,000 条指令，其中一部分指令因为等数据或分支预测失败要停几个周期")
    rows = []
    n = 10000
    for frac, penalty, why in ((0.0, 0, "没有停顿"), (0.2, 1, "20% 的指令等上一条的结果 1 周期"),
                               (0.15, 15, "15% 是分支、全部预测失败、每次损失 15 周期"),
                               (0.15 * 0.05, 15, "同上，但分支预测准确率 95%")):
        stall = int(round(n * frac * penalty))
        _, pipe = pipeline_cycles(n, stages, stall)
        rows.append([why, stall, pipe, f"{pipe / n:.3f}"])
    print_table(["情形", "停顿周期", "总周期", "CPI"], rows)
    print_observations([
        "流水线不会让一条指令变快（单条仍要 5 个周期），但指令多时几乎每个周期完成一条：吞吐提高约 5 倍。",
        "'延迟'和'吞吐'是两回事，这对概念会贯穿全课：decode 的每个 token 有延迟，GPU 每秒产出的 token 是吞吐。",
        "停顿会吃掉流水线的好处：分支预测失败尤其贵。现代 CPU 花大量晶体管做分支预测、乱序执行来减少停顿——"
        "这些都是'控制'开销（第 1 讲 §2.5 的 70 pJ/指令里的大头）。GPU 选择了另一条路：不费力减少停顿，"
        "而是准备很多个 warp，一个停了就换另一个（P2 §4）。",
    ])


# ---------------------------------------------------------------------------
# 演示 3：依赖链与指令级并行
# ---------------------------------------------------------------------------
def demo3_dependency(add_latency=4, adds_per_cycle=2):
    """
    模型：求和循环的吞吐取决于用几个独立的累加器。实测：numpy 的 sum（无依赖）vs cumsum（有依赖）。

    【参数】add_latency：浮点加法的延迟（周期，典型值 3–4）；adds_per_cycle：每周期最多发射几条加法
        （这两个是假设的典型值，不对应某一款具体 CPU）
    【实测说明】np.sum 内部把数组分成几路分别累加再合并（numpy 的"成对求和"），没有长依赖链，
        还能用 SIMD；np.cumsum 要输出每一个前缀和，第 i 个结果必须等第 i-1 个，是一条依赖链。
        cumsum 还要把结果写回内存，所以这个对比不完全是"纯依赖"的差别，但主要的差距来自依赖链。
    """
    print_header("演示 3：依赖链——s += x[i] 为什么快不起来")
    rows = []
    for k in (1, 2, 4, 8, 16):
        thr = reduction_throughput(k, add_latency, adds_per_cycle)
        rows.append([k, f"{thr:.2f}", f"{1 / thr:.2f}"])
    print(f"模型：浮点加法延迟 {add_latency} 周期，每周期最多 {adds_per_cycle} 条加法（假设值）")
    print_table(["累加器个数", "每周期完成的加法", "每个元素的周期数"], rows)

    n = 32768                      # 128 KiB（float32），放得进 L1/L2，排除内存带宽的影响
    rep = 50 if QUICK else 200
    x = np.ones(n, dtype=np.float32)
    out = np.empty_like(x)
    t_sum = best_time(lambda: [x.sum() for _ in range(rep)]) / (n * rep)
    t_cum = best_time(lambda: [np.cumsum(x, out=out) for _ in range(rep)]) / (n * rep)
    t_add = best_time(lambda: [np.add(x, x, out=out) for _ in range(rep)]) / (n * rep)
    print(f"\n实测（{n} 个 float32，放得进 cache，重复 {rep} 次取平均）：")
    print_table(["操作", "依赖关系", "每个元素"],
                [["np.add(x, x)", "每个元素独立", f"{t_add * 1e9:.3f} ns"],
                 ["x.sum()", "多路累加后合并", f"{t_sum * 1e9:.3f} ns"],
                 ["np.cumsum(x)", "第 i 个等第 i-1 个", f"{t_cum * 1e9:.3f} ns"]])
    print_observations([
        f"只有一个累加器时，每 {add_latency} 个周期才能完成一次加法——硬件明明每周期能做 {adds_per_cycle} 次，却闲着等结果。",
        f"用 {add_latency * adds_per_cycle} 个以上独立的累加器就能把加法单元喂饱：同时有多条'在路上'的依赖链，"
        "这叫指令级并行（ILP）。编译器和 numpy 的 sum 都会这样做。",
        f"实测中 cumsum 每个元素约 {t_cum / t_sum:.0f} 倍于 sum：同样读同样多的数据，有没有依赖链差一个数量级以上。",
        "同样的道理放大到 GPU：一个线程里的依赖链会让它停顿，GPU 靠'很多线程轮流执行'来填满流水线（P2 §4、§7），"
        "而 LLM 的 decode 是'一个 token 依赖上一个 token'的依赖链，只能靠批处理和投机解码来补（第 1、6 讲）。",
    ])


# ---------------------------------------------------------------------------
# 演示 4：解释器开销
# ---------------------------------------------------------------------------
def demo4_interpreter():
    """
    纯 Python 循环、Python 内置 sum + zip、numpy 的 dot，计算同一个点积，每个元素各要多久。

    【为什么差这么多】Python 是**解释执行**的：每一步 s += a[i] * b[i] 都要查变量、检查类型、
        创建新的浮点数对象、回收旧对象……几十到上百条机器指令只为了做一次乘加。
        numpy 把整个循环交给编译好的 C 代码（还用了 SIMD），每个元素只要零点几条指令。
    【系统联系】这和第 1 讲 §2.5 的"指令开销"是同一件事，只是放大了：解释器的每一步是一条"超级昂贵的指令"。
        推理引擎里同样的问题叫"CPU 开销"或"框架开销"：每启动一个 GPU 内核都要 Python + 驱动走一遍（P2 §8）。
    """
    print_header("演示 4：解释器的开销——同一个点积，三种写法")
    n = 50_000 if QUICK else 200_000
    a = [1.0] * n
    b = [2.0] * n

    def py_loop():
        s = 0.0
        for i in range(n):
            s += a[i] * b[i]
        return s

    an = np.ones(n)
    bn = np.full(n, 2.0)
    t_loop = best_time(py_loop, repeats=3) / n
    t_zip = best_time(lambda: sum(x * y for x, y in zip(a, b)), repeats=3) / n
    t_np = best_time(lambda: an @ bn, repeats=7) / n
    rows = [
        ["纯 Python for 循环", f"{t_loop * 1e9:.1f} ns", f"{t_loop / t_np:,.0f}×"],
        ["sum(x*y for x, y in zip(a, b))", f"{t_zip * 1e9:.1f} ns", f"{t_zip / t_np:,.0f}×"],
        ["numpy：an @ bn", f"{t_np * 1e9:.3f} ns", "1×"],
    ]
    print_table(["写法", "每个元素", "相对 numpy"], rows)
    print_observations([
        f"纯 Python 循环每个元素约 {t_loop * 1e9:.0f} ns，按 4 GHz 算是约 {t_loop * 4e9:.0f} 个时钟周期——一次乘加真正只要 1 个周期。",
        f"numpy 快了约 {t_loop / t_np:,.0f} 倍：把'每个元素一次解释'变成'整个数组一次调用'，固定开销被摊薄了。",
        "本讲义所有代码都尽量用 numpy 的整数组运算，原因就在这里；P2 的 GPU 模拟器也是对'一整个线程块'做 numpy 运算，"
        "而不是一个线程一个线程地用 Python 循环。",
    ])


if __name__ == "__main__":
    demo1_instructions()
    demo2_pipeline()
    demo3_dependency()
    demo4_interpreter()
