"""Chinese Manim film; all timings shown come from data/benchmarks.json.""" from pathlib import Path import json import os import re import manimpango from manim import * HERE = Path(__file__).resolve().parent DATA = json.loads((HERE / 'data/benchmarks.json').read_text()) BG, INK, MUTED = '#0B1220', '#E8EEF7', '#94A3B8' CYAN, MINT, AMBER, PANEL = '#38BDF8', '#34D399', '#FBBF24', '#172235' # WSL may use an installed Windows font without copying/distributing it. font_path = os.environ.get('SLIME_FONT_PATH', '/mnt/c/Windows/Fonts/msyh.ttc') if Path(font_path).is_file(): manimpango.register_font(font_path) fonts = manimpango.list_fonts() FONT = next((f for f in ('Microsoft YaHei', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei') if f in fonts), None) if FONT is None: raise RuntimeError('需要中文字体:安装 Noto Sans CJK,或设置 SLIME_FONT_PATH 指向中文字体。') def text(value, size=28, color=INK, max_width=12): obj = Text(str(value), font=FONT, font_size=size, color=color) if obj.width > max_width: obj.scale_to_fit_width(max_width) return obj MATH_TEMPLATE = TexTemplate( documentclass=r"\documentclass[preview]{standalone}", preamble=r"\usepackage{amsmath}\usepackage{amssymb}", ) def formula_tex(value, size=32, color=INK, max_width=12): obj = MathTex(value, tex_template=MATH_TEMPLATE, font_size=size, color=color) if obj.width > max_width: obj.scale_to_fit_width(max_width) return obj def subtitle_text(value, max_width=12.4): """Mix Pango Chinese prose and actual MathTex for $...$ inline formulas.""" if value.count('$') % 2: raise ValueError(f'Unbalanced inline math: {value}') parts = VGroup() for segment in re.split(r'(\$[^$]+\$)', value): if not segment.strip(): continue if segment.startswith('$') and segment.endswith('$'): parts.add(formula_tex(segment[1:-1], 30)) else: parts.add(text(segment.strip(), 24)) parts.arrange(RIGHT, buff=.055, aligned_edge=DOWN) if parts.width > max_width: parts.scale_to_fit_width(max_width) return parts def grid(n=5, side=.48, color=CYAN): return VGroup(*[ Square(side_length=side, stroke_color=color, stroke_width=1.3, fill_color=PANEL, fill_opacity=1).move_to( [(j-(n-1)/2)*side, ((n-1)/2-i)*side, 0]) for i in range(n) for j in range(n) ]) def box(label, width=2.5, height=.65, color=CYAN): frame = RoundedRectangle(width=width, height=height, corner_radius=.10, stroke_color=color, fill_color=PANEL, fill_opacity=1) return VGroup(frame, text(label, 22, color, width-.2)) class FilmBase(Scene): def setup(self): self.camera.background_color = BG self.cues = [] self.chapters = [] self.subtitle = None def chapter(self, number, title, tag): if self.mobjects: self.play(*[FadeOut(m) for m in list(self.mobjects)], run_time=.45) self.subtitle = None self.chapters.append({'title': title, 'start': float(self.time)}) heading = text(title, 34).to_edge(UP, buff=.35).align_to(LEFT*6.5, LEFT) badge = text(number, 20, MINT).to_corner(UR, buff=.4) footer = text(tag, 17, MUTED).move_to([0, -2.93, 0]) line = Line([-6.5, -3.16, 0], [6.5, -3.16, 0], color=PANEL) self.play(FadeIn(heading), FadeIn(badge), FadeIn(footer), Create(line), run_time=.6) def say(self, caption, seconds=5, *, display=None): if self.subtitle is not None: self.remove(self.subtitle) self.subtitle = subtitle_text(display if display is not None else caption).move_to([0, -3.58, 0]) self.add(self.subtitle) self.cues.append({'start': float(self.time), 'end': float(self.time + seconds), 'text': caption, 'display': display if display is not None else caption}) self.wait(seconds) def chart(self, key, labels): d = DATA[key] before, after = d['before_ms'], d['after_ms'] max_width = 6.4 # Exact linear scale, including very small optimized bars. baseline = -3.4 items = VGroup() for i, (label, value, color) in enumerate(zip(labels, (before, after), (AMBER, MINT))): y = .5-i*1.0 name = text(label, 24, max_width=2.5).move_to([-5, y, 0]) bar = Rectangle(width=max_width*value/before, height=.4, stroke_width=0, fill_color=color, fill_opacity=1) bar.move_to([baseline+bar.width/2, y, 0]) value_text = text(f'{value:.3f} ms', 23, color).next_to(bar, RIGHT, buff=.18) items.add(name, bar, value_text) ratio = formula_tex(rf'{before/after:.2f}\times', 46, MINT).move_to([0, -1.45, 0]) condition = text(d['workload'], 25, CYAN).move_to([0, 1.55, 0]) metric_label = { 'A': 'CPU 搜索 / GPU scan · 中位数 / 5 轮 · CPU 16 逻辑线程', 'B': 'GPU kernel · 中位数 / 5 轮 · 同组完整命中列表一致', 'C': 'GPU kernel · 中位数 / 100 轮 · 同组完整命中列表一致', }[key] metric = text(metric_label, 19, MUTED).move_to([0, -2.2, 0]) self.play(FadeIn(condition), LaggedStart(*[FadeIn(m) for m in items], lag_ratio=.08), FadeIn(ratio), FadeIn(metric), run_time=1.8) return VGroup(condition, items, ratio, metric) def opening(self): self.chapter('00', '寻找史莱姆区块:从暴力搜索到结构化计算', '示意图用于解释工作分配;测速来自实际程序') g = grid(9, .40).move_to([-3.3, -.15, 0]) title = VGroup(text('同一个问题', 38, CYAN), text('换一种计算方式', 38, MINT), text('CPU → CUDA → 复用 → 数学性质', 24, MUTED)).arrange(DOWN, buff=.4).move_to([2.3, 0, 0]) self.play(LaggedStart(*[FadeIn(c) for c in g], lag_ratio=.008), FadeIn(title), run_time=1.5) target = [g[i*9+j] for i in range(2, 7) for j in range(2, 7)] self.play(*[c.animate.set_fill(MINT, opacity=.75) for c in target], run_time=.8) self.say('我们想找一个 seed,以及一片连续 5×5 的史莱姆区块。', 6, display='我们想找一个 seed,以及一片连续 $5\\times5$ 的史莱姆区块。') self.say('先让程序算得更快,再研究随机数本身的结构。', 6) def problem(self): self.chapter('01', 'CPU:先把正确答案算出来', '源码:01_cpu.cpp · 原始 Java Random 语义 · 不通过便立即退出') g = grid(5, .62).move_to([-3.2, .15, 0]) code = VGroup(*[text(s, 25, c, 6.1) for s,c in [ ('for seed in seeds:', CYAN), (' for (x, z) in anchors:', INK), (' check 5 × 5 cells', INK), (' stop on first failure', AMBER), (' collect complete hits', MINT)]]).arrange(DOWN, aligned_edge=LEFT, buff=.26).move_to([2.5, .25, 0]) self.play(FadeIn(g), FadeIn(code), run_time=1) self.play(g[0].animate.set_fill(MINT, opacity=.6), g[1].animate.set_fill(AMBER, opacity=.8), run_time=.8) self.say('对每个 seed,遍历锚点,再判断锚点周围的 25 格。', 6) self.say('一格不满足就停止,只有全部通过才记录结果。', 6) note = text('CPU 基线也使用多线程:实测为 16 个逻辑线程', 23, MUTED).move_to([0, -2.1, 0]) self.play(FadeIn(note)) self.say('保留 CPU 实现,后面的优化都要核对相同搜索结果。', 6) def cuda(self): self.chapter('02', 'CUDA:每个 thread 负责一个 seed', '源码:02_cuda_seed.cuh · 图中只展示少量线程,实际启动更多线程') workers = VGroup(*[box(f'thread {i}', 1.45, .65) for i in range(6)]).arrange(RIGHT, buff=.3).move_to([0, 1.3, 0]) seeds = VGroup(*[text(f'seed {i}', 23, MINT) for i in range(6)]).arrange(RIGHT, buff=.75).move_to([0, .2, 0]) maps = VGroup(*[grid(3, .24) for _ in range(6)]).arrange(RIGHT, buff=1.0).move_to([0, -1.1, 0]) arrows = VGroup(*[Arrow(w.get_bottom(), s.get_top(), buff=.1, color=MUTED) for w,s in zip(workers,seeds)]) self.play(FadeIn(workers), FadeIn(seeds), FadeIn(maps), Create(arrows), run_time=1.5) for cell in range(3): self.play(*[g[cell].animate.set_fill(MINT, opacity=.65) for g in maps], run_time=.35) self.say('把 seed 分给 GPU 线程;每个线程仍然遍历整个坐标窗口。', 7) self.say('相同的检查逻辑,被更多线程同时执行。', 5) self.play(FadeOut(VGroup(workers, seeds, maps, arrows)), run_time=.6) self.chart('A', ('CPU', 'CUDA / seed')) self.say('A 组:CPU 搜索 3550 毫秒,GPU scan 111 毫秒,约快 32 倍。', 8) self.say('这里比较的是搜索时间和 GPU scan,不能改拿 kernel 时间计算这个倍数。', 7) def mapping(self): self.chapter('03', '改变分工:固定坐标,遍历 seed', '源码:03_cuda_position.cuh · 坐标项存在线程私有数组中 · 不引入全局坐标表') self.coordinate_identity() left = VGroup(box('thread → seed', 3.2, color=AMBER), text('for each (x, z)', 25), text('compute F(x+dx), G(z+dz)', 22, AMBER)).arrange(DOWN, buff=.35).move_to([-3.4, .6, 0]) right = VGroup(box('thread → (x, z)', 3.2, color=MINT), text('precompute 5 X + 5 Z terms', 22, MINT), text('for each seed: reuse terms', 23)).arrange(DOWN, buff=.35).move_to([3.1, .6, 0]) arrow = Arrow([-1.2, .5, 0], [1.0, .5, 0], color=CYAN) self.play(FadeIn(left), FadeIn(right), GrowArrow(arrow), run_time=1.2) formula = formula_tex(r'T(x+\Delta x,z+\Delta z)=F(x+\Delta x)+G(z+\Delta z)', 34, CYAN).move_to([0, -1.7, 0]) self.play(FadeIn(formula)) self.say('坐标固定之后,X 和 Z 的多项式在 seed 循环外计算。', 7, display='坐标固定之后,$X$ 和 $Z$ 的多项式在 seed 循环外计算。') self.say('同一份坐标项,可以在许多 seed 上重复使用。', 6) self.play(FadeOut(VGroup(left, right, arrow, formula))) self.chart('B', ('CUDA / seed', 'CUDA / coordinate')) self.say('B 组扩大到 4097×4097;相同任务下,从 31.599 秒降到 17.600 秒。', 8, display='B 组扩大到 $4097\\times4097$;相同任务下,从 31.599 秒降到 17.600 秒。') self.say('这一组重新测量两个版本,只在组内比较:约快 1.80 倍。', 7) def outer(self): self.chapter('04', 'Outer sum:邻居也可以共享坐标项', '源码:04_cuda_outer_sum.cuh · 实际 block 为 16×16 · 示意图简化为 5×5') self.outer_identity() g = grid(5, .54).move_to([0, .1, 0]) xs = VGroup(*[formula_tex(rf'F_{{{i}}}', 28, CYAN).move_to([g[i*5].get_x()-.75, g[i*5].get_y(), 0]) for i in range(5)]) zs = VGroup(*[formula_tex(rf'G_{{{i}}}', 28, MINT).move_to([g[i].get_x(), g[i].get_y()+.65, 0]) for i in range(5)]) self.play(FadeIn(g), FadeIn(xs), FadeIn(zs), run_time=1) self.play(*[g[2*5+j].animate.set_fill(CYAN, opacity=.4) for j in range(5)], run_time=.7) self.say('固定 X 的坐标项沿一整行复用;固定 Z 的项沿一整列复用。', 7, display='固定 $X$ 的坐标项沿一整行复用;固定 $Z$ 的项沿一整列复用。') self.play(*[g[i*5+3].animate.set_fill(MINT, opacity=.4) for i in range(5)], run_time=.7) formula = formula_tex(r'T_{ij}=X_i+Z_j', 36, MINT, 4.5).move_to([3.9, -.1, 0]) self.play(FadeIn(formula)) self.say('把两条轴放进 shared memory,再用 outer sum 组合每格的输入。', 7) self.play(FadeOut(VGroup(g, xs, zs, formula))) note = text('16×16 锚点 + 5×5 图案 → 每条轴需要 20 项(包含 halo)', 22, MUTED).move_to([0, 2.1, 0]) self.add(note) self.chart('C', ('fixed coordinate', 'tiled outer sum')) self.say('C 组固定一个 seed;kernel 从 2.715 毫秒降到 1.598 毫秒。', 8) self.say('重新测量 03 再比较 04,约快 1.70 倍;不同负载的倍数不能连乘。', 7) def coordinate_identity(self): intro = text('先看送入 Random 之前的坐标加和项', 25, MUTED).move_to([0,2.05,0]) original = formula_tex( r'T(x,z)=\operatorname{J}_{32}(4987142x^2)+\operatorname{J}_{32}(5947611x)' r'+4392871\operatorname{J}_{32}(z^2)+\operatorname{J}_{32}(389711z)', 34).move_to([0,1.25,0]) wrap = VGroup( formula_tex(r'\operatorname{J}_{32}(a)=((a+2^{31})\bmod 2^{32})-2^{31}', 30, MUTED), text('32 位回绕 → 有符号解释 → 提升到 64 位', 21, MUTED), ).arrange(DOWN, buff=.15).move_to([0,-1.9,0]) self.play(FadeIn(intro), FadeIn(original), FadeIn(wrap), run_time=1.1) self.say('把原始坐标公式写完整,并保留 Java 的 32 位回绕位置。', 7) x_part = formula_tex( r'F(x)=\operatorname{J}_{32}(4987142x^2)+\operatorname{J}_{32}(5947611x)', 36, CYAN).move_to([0,.35,0]) z_part = formula_tex( r'G(z)=4392871\operatorname{J}_{32}(z^2)+\operatorname{J}_{32}(389711z)', 36, MINT).move_to([0,-.55,0]) self.play(FadeIn(x_part), FadeIn(z_part), run_time=1) self.say('前两项只依赖 X,后两项只依赖 Z,把它们分别命名为 F 和 G。', 8, display='前两项只依赖 $X$,后两项只依赖 $Z$,把它们分别命名为 $F$ 和 $G$。') separated = formula_tex(r'T(x,z)=F(x)+G(z)', 44, AMBER).move_to(original) self.play(Transform(original, separated), run_time=1) self.say('没有 X 与 Z 相乘的混合项,所以坐标输入能精确拆成两条独立的轴。', 8, display='没有 $X$ 与 $Z$ 相乘的混合项,所以坐标输入能精确拆成两条独立的轴。') self.say('这只是按变量重新分组,不是近似;每一项的整数回绕规则都保持原样。', 7) self.play(FadeOut(VGroup(intro,original,x_part,z_part,wrap)), run_time=.6) def outer_identity(self): entry = formula_tex(r'T_{ij}=F(x_i)+G(z_j)=X_i+Z_j', 42, CYAN).move_to([0,1.8,0]) vectors = formula_tex( r'\mathbf X=(X_0,\ldots,X_{m-1})^{\mathsf T},\qquad' r'\mathbf Z=(Z_0,\ldots,Z_{n-1})^{\mathsf T}', 32, MUTED).move_to([0,.9,0]) identity = formula_tex( r'\mathbf T=\underbrace{\mathbf X\mathbf 1_n^{\mathsf T}}_{\text{repeat X across columns}}' r'+\underbrace{\mathbf 1_m\mathbf Z^{\mathsf T}}_{\text{repeat Z across rows}}', 40, MINT).move_to([0,-.2,0]) self.play(FadeIn(entry), FadeIn(vectors), FadeIn(identity), run_time=1.3) self.say('对所有 X 和所有 Z 配对,第 i 行、第 j 列就是 X 的第 i 项加 Z 的第 j 项。', 8, display='对所有 $X$ 和所有 $Z$ 配对,第 $i$ 行、第 $j$ 列就是 $X_i+Z_j$。') self.say('矩阵写法是:X 列向量复制到每列,Z 行向量复制到每行,再逐项相加。', 8, display='矩阵写法是:$X$ 列向量复制到每列,$Z$ 行向量复制到每行,再逐项相加。') self.play(FadeOut(vectors), FadeOut(identity), run_time=.5) example = formula_tex( r'\begin{pmatrix}2&2&2\\5&5&5\end{pmatrix}' r'+\begin{pmatrix}10&20&30\\10&20&30\end{pmatrix}' r'=\begin{pmatrix}12&22&32\\15&25&35\end{pmatrix}', 40).move_to([0,.1,0]) note = text('数值仅用于演示广播相加;不是实测坐标项', 21, MUTED).move_to([0,-1.25,0]) contrast = text('outer sum:两条轴相加 · outer product:两条轴相乘', 24, AMBER).move_to([0,-2.0,0]) self.play(FadeIn(example), FadeIn(note), FadeIn(contrast), run_time=1) self.say('例如 X 是二和五,Z 是十、二十、三十,就得到这张二乘三的加法表。', 8, display='例如 $X=(2,5)^{\\mathsf T}$,$Z=(10,20,30)^{\\mathsf T}$,就得到这张 $2\\times3$ 的加法表。') self.say('两项 X 加三项 Z,只需五次轴函数计算;六个格子仍需六次相加。', 8, display='两项 $X$ 加三项 $Z$,只需五次轴函数计算;六个格子仍需六次相加。') self.say('这就是 outer sum:所有交叉组合都相加。外积则是相乘,运算不同。', 7) self.say('我们复用的是 Random 之前的坐标项,仍要逐格执行随机数判断。', 8) self.play(FadeOut(VGroup(entry,example,note,contrast)), run_time=.6) def random(self): self.chapter('05', '研究 Random:从计算速度转向状态空间', '性质章节独立于 01–04 的测速 · seed 均指有效的 48-bit 状态等价类') bits = VGroup(*[Rectangle(width=.19, height=.55, stroke_width=.7, stroke_color=BG, fill_color=AMBER if i<16 else CYAN, fill_opacity=1) for i in range(64)]).arrange(RIGHT, buff=.015).move_to([0, 1.25, 0]) labels = VGroup(text('高 16 位:不影响 Random 结果', 24, AMBER).move_to([-4.0,.3,0]), text('低 48 位:决定内部状态', 24, CYAN).move_to([2,.3,0])) self.play(FadeIn(bits), FadeIn(labels), run_time=1) formula = VGroup(formula_tex(r'u=\bigl((\mathrm{seed}+T)\mathbin{\mathtt{xor}}C\bigr)\bmod 2^{48}', 34, CYAN), formula_tex(r'v=(Au+11)\bmod 2^{48}', 34), formula_tex(r'q=\left\lfloor\frac{v}{2^{17}}\right\rfloor', 34, MINT)).arrange(DOWN, buff=.25).move_to([0, -1.4, 0]) self.play(FadeIn(formula)) self.say('输入是 64 位,但 Java Random 的状态只有 48 位。', 7) self.say('相差 2^48 的 seed 给出相同结果,包括拒绝重抽的路径。', 7, display='相差 $2^{48}$ 的 seed 给出相同结果,包括拒绝重抽的路径。') self.say('这不是一个微小的计时优化,而是搜索空间里的等价关系。', 7) self.play(FadeOut(VGroup(bits,labels,formula))) self.lower35_proof() def lower35_proof(self): def page(title, equations, note=None): heading = text(title, 27, CYAN).move_to([0, 2.15, 0]) rows = VGroup(*[formula_tex(eq, 32, color) for eq, color in equations]) rows.arrange(DOWN, buff=.32).move_to([0, .25, 0]) objects = VGroup(heading, rows) if note: objects.add(text(note, 21, MUTED).move_to([0, -2.25, 0])) self.play(FadeIn(objects), run_time=.7) return objects first = page('先明确未知量:不是把高 13 位忽略', [ (r's=\ell+2^{35}h,\quad 0\le\ell<2^{35},\quad0\le h<8192', CYAN), (r'u_i(h)=\bigl((s+d_i)\mathbin{\mathtt{xor}}C\bigr)\bmod2^{48}', INK), (r'C=\mathtt{0x5E434E432},\qquad 2^{34}\le C<2^{35}', MINT), ], '固定坐标求解:s 是世界种子') self.say('低 35 位是待求输入的一部分;固定它之后,还要求出高 13 位。', 8, display='$\\ell=s\\bmod2^{35}$ 是待求输入的低 35 位;固定它之后,还要求出高 13 位。') self.say('为什么选 35?合并后的 XOR 常量恰好占 35 位,不会翻转更高位。', 9) self.play(FadeOut(first)) carry = page('保留坐标进位,再分离 XOR', [ (r'(\ell+d_i)\bmod2^{48}=L_i+2^{35}H_i', INK), (r'(s+d_i)\bmod2^{48}=L_i+2^{35}\bigl((H_i+h)\bmod8192\bigr)', CYAN), (r'u_i(h)=\bigl(u_i(0)+2^{35}h\bigr)\bmod2^{48}', MINT), ], 'Hᵢ 包含坐标加法的进位;C 只 XOR 低位 Lᵢ') self.say('坐标项加到低位上可能进位,我们把进位保留在 H 里面。', 8, display='坐标项加到低位上可能进位,我们把进位保留在 $H_i$ 里面。') self.say('XOR 只改变低位,所以改变 h,相当于让初始化状态增加 h 乘 2^35。', 10, display='XOR 只改变低位,所以改变 $h$,相当于让初始化状态增加 $h\\,2^{35}$。') self.say('这个平移来自位域分离,不能把一般的 XOR 当成线性运算。', 8) self.play(FadeOut(carry)) output = page('一次状态推进:35 − 17 = 18', [ (r'v_i(h)=\bigl(v_i(0)+Ah\,2^{35}\bigr)\bmod2^{48}', INK), (r'q_i(h)=\bigl(q_i(0)+Ah\,2^{18}\bigr)\bmod2^{31}', CYAN), (r'q_i(0)=r_i+2^{18}b_i', INK), (r'q_i(h)=r_i+2^{18}\bigl((b_i+Ah)\bmod8192\bigr)', MINT), ], '0 ≤ rᵢ < 2¹⁸;0 ≤ bᵢ < 8192;所有格子共用同一个 h') self.say('LCG 先乘 A。next(31) 再去掉状态低 17 位,因此 2^35 变成 2^18。', 11, display='LCG 先乘 $A$。next(31) 再去掉状态低 17 位,因此 $2^{35}$ 变成 $2^{18}$。') self.say('把首次输出拆成 r 和 b:高位 h 只改变 b,输出的低 18 位 r 保持不变。', 10, display='把首次输出拆成 $r_i$ 和 $b_i$:高位 $h$ 只改变高位部分,低 18 位 $r_i$ 保持不变。') self.play(FadeOut(output)) modular = page('模 10 条件 = 奇偶条件 + 模 5 条件', [ (r'y_i=(b_i+Ah)\bmod8192,\qquad q_i=r_i+2^{18}y_i', INK), (r'2^{18}\equiv0\pmod2,\qquad2^{18}\equiv-1\pmod5', CYAN), (r'q_i\equiv0\pmod{10}\iff r_i\equiv0\pmod2\ \land\ y_i\equiv r_i\pmod5', MINT), ], '这里先求 q mod 10 = 0 的候选;Java 拒绝重抽在后面单独说明') self.say('如果 r 是奇数,所有 h 都无法让首次输出被 10 整除。', 8, display='如果 $r_i$ 是奇数,所有 $h$ 都无法让首次输出被 10 整除。') self.say('r 为偶数时,因为 2^18 模 5 等于负一,剩下的要求就是 y 与 r 模 5 相同。', 11, display='$r_i$ 为偶数时,因为 $2^{18}\\equiv-1\\pmod5$,剩下的要求就是 $y_i\\equiv r_i\\pmod5$。') self.play(FadeOut(modular)) intervals = page('换变量:每个格子最多回绕一次', [ (r't=Ah\bmod8192,\qquad h=4965t\bmod8192', CYAN), (r'y_i=(b_i+t)\bmod8192', INK), (r'\begin{cases}t\equiv r_i-b_i\pmod5,&t<8192-b_i,\\' r't\equiv r_i-b_i+2\pmod5,&t\ge8192-b_i.\end{cases}', MINT), ], 'A 是奇数,模 8192 可逆;回绕后 +2 来自 8192 mod 5 = 2') self.say('把 A 乘 h 换成 t。A 是奇数,这个换元一一对应,不会丢掉高位解。', 10, display='令 $t=Ah\\bmod8192$。$A$ 是奇数,这个换元一一对应,不会丢掉高位解。') self.say('b 加 t 最多回绕一次,所以每格只有两段固定的模 5 要求。', 9, display='$b_i+t$ 最多回绕一次,所以每格只有两段固定的模 5 要求。') self.play(FadeOut(intervals)) example = page('数值例子:看见回绕怎样改变要求', [ (r'r=2,\quad b=8190,\quad8192-b=2', CYAN), (r'0\le t<2:\ t\equiv2\pmod5\quad\Longrightarrow\quad\varnothing', INK), (r'2\le t<8192:\ t\equiv4\pmod5\quad\Longrightarrow\quad t=4,9,14,\ldots', MINT), (r't=4:\quad y=2,\quad q=2+2^{18}\cdot2=524290', AMBER), ], 'r、b 为示意数值;取 t=4 时恢复 h=3476') self.say('例如 r 等于二、b 等于 8190,t 到二就回绕,允许的余数从二变成四。', 11, display='例如 $r_i=2$、$b_i=8190$,$t=2$ 时回绕,允许的余数从二变成四。') self.say('取 t 等于四,输出为 524290,确实能被十整除。', 8, display='取 $t=4$,输出为 $q=524290$,确实能被十整除。') self.play(FadeOut(example)) joint = page('多个格子:按回绕点分段,再求交', [ (r't\in[a_j,a_{j+1}),\qquad t\equiv c_i\pmod5\ \ (\forall i)', INK), (r'c_i\ne c_k\ \Longrightarrow\ \varnothing', AMBER), (r'c_i=c\ \ (\forall i)\ \Longrightarrow\ t=c+5k\in[a_j,a_{j+1})', MINT), (r'\underbrace{18}_{\mathrm{parity}}+\underbrace{17}_{\mathrm{enumerate}}' r'+\underbrace{13}_{\mathrm{solve}}=48', CYAN), ], '输入低 18 位决定首次输出奇偶;再补上中间 17 位得到低 35 位;最后求高 13 位') self.say('排序所有格子的回绕点。每段内,只要两格要求不同余数,就能丢掉整段。', 10) self.say('要求一致时,直接按间隔五生成 t,再恢复 h,不必逐个检查 8192 个高位。', 11, display='要求一致时,直接按间隔五生成 $t$,再恢复 $h$,不必逐个检查 8192 个高位。') self.say('低 18 位先筛奇偶,中间 17 位目前仍需扩展;低 35 位候选的生成成本没有消失。', 10, display='低 18 位先筛奇偶,中间 17 位仍需扩展;$\\ell=s\\bmod2^{35}$ 的供给成本没有消失。') self.play(FadeOut(joint)) boundary = page('公式精确,但候选条件有适用边界', [ (r'q<2^{31}-8:\quad\mathrm{nextInt}(10)=q\bmod10', MINT), (r'q\ge2^{31}-8:\quad\mathrm{reject\ and\ redraw}', AMBER), (r'2147483640\bmod10=0\quad\text{but this output is rejected}', INK), ], '精确验证能去掉假阳性,不能补回首次输出筛选漏掉的重抽成功解') self.say('Java 会拒绝最高八个首次输出并重抽;此时不能只看首次输出模十。', 10) self.say('所以这里是首次输出候选模型,还不能宣称穷尽了所有 Java Random 解。', 9) def ending(self): self.chapter('06', '优化的两条线:复用计算,利用结构', DATA['hardware']) rows = VGroup(*[text(s, 29, c) for s,c in [ ('CPU → CUDA:把独立任务同时做', CYAN), ('固定坐标 → outer sum:减少重复的坐标计算', MINT), ('Random → 高位解析:利用输出的模运算结构', AMBER), ('每个加速比,都要有同任务、同输入的对照', INK)]]).arrange(DOWN, aligned_edge=LEFT, buff=.55).move_to([0,.2,0]) self.play(LaggedStart(*[FadeIn(r) for r in rows], lag_ratio=.2), run_time=1.8) self.say('先理解重复工作在哪里,再决定并行、复用,还是改变搜索方法。', 7) self.say('源码、完整测试口径和原始测速记录,都放在 show 目录里。', 6) def tear_down(self): output = HERE / 'output' output.mkdir(exist_ok=True) name = self.__class__.__name__ (output / f'{name}.timeline.json').write_text(json.dumps( {'duration': float(self.time), 'chapters': self.chapters, 'cues': self.cues}, ensure_ascii=False, indent=2)+'\n') def stamp(t): ms = round(t*1000) return f'{ms//3600000:02}:{ms//60000%60:02}:{ms//1000%60:02},{ms%1000:03}' (output / f'{name}.srt').write_text('\n\n'.join( f'{i}\n{stamp(c["start"])} --> {stamp(c["end"])}\n{c["text"]}' for i,c in enumerate(self.cues,1))+'\n') (output / f'{name}.narration.md').write_text('# 口播稿(与当前渲染时间轴一致)\n\n'+ '\n\n'.join(f'[{stamp(c["start"])}] {c["text"]}' for c in self.cues)+'\n') super().tear_down() class SlimeFilm(FilmBase): def construct(self): for part in (self.opening, self.problem, self.cuda, self.mapping, self.outer, self.random, self.ending): part() class ParallelEpisode(FilmBase): def construct(self): for part in (self.opening, self.problem, self.cuda, self.mapping, self.outer): part() class RandomEpisode(FilmBase): def construct(self): for part in (self.random, self.ending): part()