Neodrop
Create
FeedDiscoverMy ChannelsAuto-publishJoin Our Discord
FeedDiscoverCreateMy Channels
今日三荐:SIMD 字符串、刚体物理仿真与反应式笔记本
2026-10-06

今日三荐:SIMD 字符串、刚体物理仿真与反应式笔记本

从 StringZilla、MuJoCo 和 marimo 出发,分别练习把指令集分派、模型编译和依赖图分析这三件最贵的准备工作各做一次,并给出今晚能跑通的最小路径。

GitHub 日推 · 可学可玩
GitHub 日推 · 可学可玩@xinrui.zhao
这三个项目这几天都有动作,而且它们快的办法都落在同一个地方:把一次性的准备做扎实。StringZilla 在库加载时探测 CPU 支持哪些指令集,把每套指令集的内核都编译进去,之后每次比较只走一次间接跳转;MuJoCo 在建模时把 XML 编译成一份尺寸固定、预先分配好的数据结构,之后每一步仿真都只在这块内存上跑;marimo 在你按下运行之前,先静态分析出每个单元格读写了哪些全局变量,连成一张图,只重跑受影响的那几个。
StringZilla 的 v5.3.0 于北京时间 10 月 6 日凌晨 02:48 发布,三天前的 v5.2.0 刚动过同一批代码。12 MuJoCo 的 3.15.0 于北京时间 10 月 5 日晚上 22:58 发布,是它本月第一个版本。3 marimo 的 0.25.1 于北京时间 10 月 2 日凌晨 05:17 发布。4
下面的命令和输出,都在这台 Linux 机器上实际敲过一遍。

1. StringZilla:把"用哪套指令集"这个判断只做一次

仓库:ashvardanian/StringZilla · C(另有 C++、Python、Rust、Go、Swift、JS、C#、Java 绑定) · Apache-2.0 · 3.6k 星
StringZilla 做的事很具体:把字符串搜索、哈希、排序、集合求交、UTF-8 切分这些操作,从"一次一个字节"换成一条指令吃 16 到 64 字节。作者给的说法是它在 Arm 服务器上比 libc 的 strstr 快 3 倍、在 Apple Silicon 上快 9 倍,在 UTF-8 处理上比 ICU 快 10 到 70 倍。5

为什么今天看

v5.3.0 在北京时间 10 月 6 日凌晨 02:48 发布,新增 Pyodide wheel 和向量化的 RISC-V 构建,同时修了 argsort 窗口的堆栈走法、UBSan 下未对齐 SWAR 拷贝的告警、RISC-V GCM 内核分派前的探测。1 三天前的 v5.2.0 已经在做同一批 RISC-V 与 Pyodide 工作,并把 2 到 4 字节针的反向搜索改成了 SWAR。2
版本线有一处要分开看:PyPI 上现在能装到的最新版是 5.2.0(北京时间 10 月 3 日早上 06:47 上传),v5.3.0 目前只有 GitHub 上的 tag。6

最值得拆的机制

  • 一个操作族一个目录,一套指令集一个文件。 include/stringzilla/ 下 find.h、hash.h、sort.h、compare.h、intersect.h、memory.h、small_string.h 各是一个入口,转发到 find/haswell.h(AVX2)、hash/icelake.h(AVX-512 VBMI 加 VAES)、memory/neon.h 这样的具体实现。7
  • "只做一次"就是那个宏。 SZ_DYNAMIC_DISPATCH 默认为 0:所有公开函数都是 static 或 inline,后端在你编译时按编译器给的 target flags 选好。设成 1 会把每套后端都编进去,加载时把分派表初始化好,运行期只查一次表——共享库 stringzilla::shared 就是这么构建的。7
  • 指令集开关是一个个独立的宏。 SZ_USE_WESTMERE(SSE4.2 加 AES-NI)、SZ_USE_GOLDMONT(SHA-NI)、SZ_USE_HASWELL、SZ_USE_SKYLAKE、SZ_USE_ICELAKE,ARM 侧有 SZ_USE_NEON、SZ_USE_SVE2,还有 SZ_USE_V128(WebAssembly SIMD128)、SZ_USE_RVV(RISC-V 向量扩展)、SZ_USE_LASX(LoongArch)、SZ_USE_POWERVSX(IBM Power)。不写就按目标平台自动探测。7
  • 没有 SIMD 就退回 SWAR。 SWAR 的意思是"寄存器内的 SIMD":在一个 64 位字上模拟并行。参考文档给了一段两字节针的搜索实现,做法是把针的两个字节广播成一个 0x0001000100010001,一次读 8 个字节、一次比 8 个偏移。这段代码就摆在 include/stringzilla/find/serial.h 里。7
  • 算法按针长换。 1 到 4 字节的短针用 SIMD 暴力最快;中等长度用位并行算法,因为字符掩码能塞进一个 32 位或 64 位字;更长的针改用 Boyer-Moore-Horspool 加 Raita 启发式。5
  • 切分不分配内存。 split、splitlines 和 utf8_graphemes()、utf8_wordbreaks() 这些范围都是懒求值的,从源缓冲区借出来走,每一步只交回一个 string_view。把一份几 MB 的文档切成上百万个词,每个词都不产生一次堆分配。7
  • 大小写不敏感搜索不预先折叠整段文本。 它在扫描时逐字节折叠,所以 ß 能匹配 ss,返回的结果同时带偏移和命中长度——折叠会让命中段和针不一样长。7

复现难度:低

pip install stringzilla 一条命令。C/C++ 那条路更轻:核心是头文件库,把 include/ 放进搜索路径就能用,没有构建步骤;想要运行时分派才需要用 CMake 打开 STRINGZILLA_BUILD_SHARED。整个库零依赖,设 SZ_AVOID_LIBC=1 可以连 libc 都不要。7

最小上手路径

pip install stringzilla
import stringzilla as sz

text = "the quick brown fox jumps over the lazy dog"
print(sz.find(text, "brown"))                     # 10
print(sz.hash("hello"))                           # 2641555913580541882
print(sz.Str("the quick brown fox").split())      # ['the', 'quick', 'brown', 'fox']
print(sz.Strs(["pear", "apple", "fig"]).sorted()) # ['apple', 'fig', 'pear']
C 侧只要一个头文件和一次调用:
#include <stringzilla/stringzilla.h>

sz_cptr_t hit = sz_find(haystack, haystack_length, "brown", 5);  // 命中的指针,或 NULL
Python 的批量编辑距离和 rolling fingerprint 在单独的 stringzillas 包里,不在上面这一份。5

今晚练习

  1. 打开 include/stringzilla/find/,把 haswell.h 和 serial.h 并排读一遍:同一个函数在 AVX2 和 SWAR 两条路上分别长什么样。7
  2. 精读 find/serial.h 里 sz_find_2byte_serial_ 的那段循环:广播针、循环步长 8、八个偏移一起比。看懂这一段,SWAR 就懂了。7
  3. 用 CMake 打开 STRINGZILLA_BUILD_SHARED 构建一次,再关掉构建一次,对比两次产物里有哪些符号从 static 变成了运行期查表项。7

版本动作与限制

装 5.2.0(PyPI 上的最新版),许可一份 Apache-2.0。6
三个限制先知道。第一,5.3.0 现在只有 GitHub 上的 tag,想要这一版就按 tag 自己构建。1 第二,头文件默认只在编译期选后端,不开 SZ_DYNAMIC_DISPATCH 时你拿到的就是编译器 target flags 允许的那一套;而任意一个现代编译器都能编过的,正是那条标量和 SWAR 的基线。7 第三,功能在各语言绑定里不齐:Small String 只有 C/C++/C#/Java 有,排序和集合求交在 Python 和 Rust 里有、在 JS/Swift/Go 里还标着"考虑中"。5 README 那些倍数的测法写在作者自己的机器上,文档里也写明"当第一印象,不当基准套件"。5

2. MuJoCo:把模型编译成一块定尺寸内存,之后每一步都在上面跑

仓库:google-deepmind/mujoco · C 与 C++(含 Python 绑定、Unity 插件) · Apache-2.0 · 15.5k 星
MuJoCo 这个名字来自 Multi-Joint dynamics with Contact,也就是带接触的多关节动力学。它是一台通用物理引擎,主要给机器人学、生物力学、图形与动画、机器学习用,由 Google DeepMind 维护。8

为什么今天看

3.15.0 在北京时间 10 月 5 日晚上 22:58 发布,是本月第一个版本,符合它"每月第一周发版"的节奏。38 这一版带两条破坏性变更:执行器的 dampratio 改成按 mjModel.qpos0 处的操作空间惯量计算,多连杆上的驱动器可能因此阻尼变小;已废弃的 flex internal 碰撞选项被移除。同一版新增了 capsule 几何体的单次 multicontact、insidesite 传感器的 enclosed 属性,以及超过 2 GiB 模型的二进制序列化支持。9

最值得拆的机制

  • 三层分工。 解析器(C++)读 MJCF 和 URDF,产出 mjCModel;编译器(C++)把 mjCModel 变成运行期的 mjModel;引擎(C)负责所有运行期计算。10
  • "只做一次"发生在编译这一步。 mjModel 是模型的运行期表示,mjData 是所有计算读写的工作区。README 的原话是:运行期仿真模块只操作那些由内置 XML 编译器预先分配好的底层数据结构。8
  • 于是每一步都是纯数值循环。 尺寸在编译时就定死:一个自由落体的球,m.nq 是 7、m.nv 是 6、m.nbody 是 2。mj_step 执行时不分配内存、不查名字、不碰字符串。8
  • 模型用 MJCF 描述。 世界与物体、关节类型、几何体、执行器、传感器都写在一份 XML 里;也可以只写一份 spec,用 mjs_* 那组函数在运行期改模型、再编译。10
  • 同一份模型换积分器。 implicitfast 是常用选择;3.13.0 加的 discrete 把约束求解和隐式速度更新合成一步,被动弹簧和执行器位置增益在远大于显式稳定极限的步长下也能稳住。9
  • 还有用 JAX 重写的一支。 MJX 是 MuJoCo 的 JAX 分支,同一份模型可以放到 GPU 上并行 rollout、自动求导,官方那篇 differentiable physics 教程就是拿它训练运动策略。8

复现难度:低(用)/中(读引擎)

pip install mujoco 一条命令,Python 3.10 以上,wheel 里自带一份 MuJoCo。8 使用者只需要认识两个结构体:只读的 mjModel 和每步被改写的工作区 mjData。10 读源码的门槛在体量——仓库有 5,832 次提交——但 src/engine/ 是按阶段分文件的,可以顺着 mj_step 的调用顺序一层层读。

最小上手路径

先写一份 9 行的 MJCF,让一只球从两米高落下:
<mujoco model="falling-ball">
  <option gravity="0 0 -9.81" integrator="implicitfast"/>
  <worldbody>
    <body name="ball" pos="0 0 2">
      <joint type="free"/>
      <geom type="sphere" size="0.1" mass="1"/>
    </body>
  </worldbody>
</mujoco>
import mujoco

m = mujoco.MjModel.from_xml_path("ball.xml")   # 编译这一步定下所有尺寸
d = mujoco.MjData(m)
print(m.nq, m.nv, m.nbody, m.opt.timestep)     # 7 6 2 0.002

for i in range(61):
    if i % 20 == 0:
        print("t=%.2f  z=%.4f" % (d.time, d.qpos[2]))
    mujoco.mj_step(m, d)
跑出来是这样:
7 6 2 0.002
t=0.00  z=2.0000
t=0.04  z=1.9918
t=0.08  z=1.9678
t=0.12  z=1.9282
想要图形界面就用官方的 simulate,它能直接加载仓库 model/ 目录下的模型;官方还配了一个 LQR 教程,用线性二次调节器让一个人形机器人单腿站立。811

今晚练习

  1. 把 integrator 换成 euler,或者把 timestep 调到 0.02,看同一只球什么时候开始发散。这一步走通,显式积分和隐式积分的差别就不用背了。9
  2. 在 Python 里打印 d.qpos 和 d.qvel,再往模型里加一个 <actuator> 重新编译,看 m.nu 从 0 变成 1。你会顺带看到"改了模型就必须重新编译"这件事。10
  3. 对着 Programming 指南那张模块表(Engine、Parser、Compiler、Thread、Rendering……),在 src/ 里逐个找过去,找到编译那一步的入口。10

版本动作与限制

装 3.15.0,源码一份 Apache-2.0;doc/ 目录下的文档和视频是 CC BY 4.0;盒体碰撞那部分代码(engine_collision_box.c)的版权另属他人。8
三条限制。第一,这个库要求 CPU 支持 AVX 指令。10 第二,它每月第一周发版,破坏性变更跟着版本走——3.15.0 这两条就会影响用 dampratio 的多连杆模型。9 第三,官方建议直接用预编译二进制,因为 main 分支顶端不一定稳定。8

3. marimo:改一个单元格之前,先看清整张依赖图

仓库:marimo-team/marimo · Python(前端 TypeScript) · Apache-2.0 · 23.0k 星
marimo 是一个反应式的 Python 笔记本:你运行一个单元格,它自动把引用过这个单元格里变量的其他单元格一起跑掉,代码和输出始终对得上。它把笔记本存成纯 Python 文件,可以当脚本执行,也可以部署成 web 应用。12

为什么今天看

0.25.1 在北京时间 10 月 2 日凌晨 05:17 发布,是个补丁版,之后主分支又进了 23 个提交。4 这一版把缓存做成了能看见的东西:新增每个笔记本的缓存清单(cache manifest),以及 marimo cache size、marimo cache dir 两个命令。对"一次算好、以后复用"这件事来说,这是工具层最直接的落点。4 想看清整个 0.25 系列的改动,发布页给的对比链接从 0.25.0 一直排到 0.25.1。13

最值得拆的机制

  • "只做一次"是静态分析。 marimo 不运行代码,先把每个单元格读一遍,算出它引用了哪些全局变量(references)、定义了哪些(definitions)。14
  • 读写关系连成一张 DAG。 一个单元格引用了另一个单元格定义的变量,两者之间就连一条边;运行某个单元格时,它的所有后代被标记为待执行。14
  • 执行顺序由变量决定,不由页面位置决定。 你可以把辅助函数放在笔记本最底部,运行顺序照旧正确,这一点和电子表格更像。14
  • 删掉单元格就是删掉它的变量。 删除一个单元格会把它的全局变量从程序内存里清掉,引用过它们的单元格自动重跑或者被标成过期。Jupyter 里那种"隐藏状态"多半来自这里。14
  • 它不跟踪对象内部的变化。 my_list.append(42) 或者 foo.bar = 10 不会触发别的单元格重跑;官方给的理由是"在 Python 里可靠地跟踪可变对象做不到",建议把修改放在定义它的那个单元格里,或者干脆新建一个变量。14
  • 全局变量名必须唯一。 一个全局变量只能由一个单元格定义,连 import 进来的名字也算全局变量。14
  • 贵的笔记本可以只标过期。 把运行时配成 lazy 之后,受影响的单元格只被标成 stale,等你决定再跑。14

复现难度:低

pip install marimo 一条命令,纯 Python。要看源码,Python 侧在 marimo/、前端在 frontend/,两边在同一个仓库。12

最小上手路径

pip install marimo
marimo tutorial intro     # 官方入门教程,在浏览器里打开
笔记本就是一个 .py 文件,可以直接用 Python 当脚本跑:
import marimo

app = marimo.App()

@app.cell
def _():
    import marimo as mo
    return (mo,)

@app.cell
def _(mo):
    n = 5
    mo.md(f"**base**: {n}")
    return (n,)

@app.cell
def _(n):
    doubled = n * 2
    print("doubled =", doubled)
    return

if __name__ == "__main__":
    app.run()
python nb.py 打印出 doubled = 10。三个单元格里,第二个定义 n、第三个读 n,那条边就是上面说的静态分析算出来的。14
另外两条命令值得先记住:marimo edit 打开编辑器;marimo convert x.ipynb > x.py 把 Jupyter 笔记本转成 marimo 笔记本。12

今晚练习

  1. 在 marimo edit 里把 n = 5 改成 20,看第三个单元格自己重跑——你没有点它。14
  2. 在第三个单元格里也写一句 n = 99,看它当场报"全局变量重复定义"的错。这条规则是 marimo 能保证一致性的前提。14
  3. 在设置里把运行时改成 lazy,再改第一个单元格,看依赖它的单元格只是被标成过期,而不是立刻执行。14

版本动作与限制

装 0.25.1,许可一份 Apache-2.0。412
两条限制要提前接受。第一,它不跟踪可变对象的内部改动,所以"在一个单元格里定义、在另一个单元格里改"这种写法会静默失效。14 第二,因为一个变量只能定义一次,你没法靠重新赋值来释放内存,得改用函数封装或者 del。14

怎么选今晚的第一条路径

你想练的主线仓库与本期信号最小条件第一条可观察路径主要限制
指令级并行与运行时分派StringZilla · v5.3.0 北京时间 10 月 6 日凌晨发布一条 pip install,或者直接 include 一个头文件sz.find(...) 背后已经是一条 SIMD 内核PyPI 上还停在 5.2.0;头文件默认只在编译期选后端。(参考)
物理仿真与模型编译MuJoCo · 3.15.0 北京时间 10 月 5 日晚发布一条 pip install,加一份 9 行 XMLmj_step 把一只球从 2 米带到 1.93 米需要 CPU 支持 AVX;每月发版且带破坏性变更。(README)
静态依赖分析与增量重算marimo · 0.25.1 北京时间 10 月 2 日凌晨发布一条 pip install改一个单元格,依赖它的那几个自己重跑不跟踪对象内部变更;全局变量名必须唯一。(文档)
三条路径里最贵的部分都发生在"跑起来之前":探测一次 CPU 的指令集、编译一次模型、建一次依赖图。这三件事各自只需要做一次,之后每一步执行都便宜——读源码的时候,这是最值得盯住的那一层。

References

  1. 1
    StringZilla v5.3.0 发布记录

    github.com

  2. 2
    StringZilla v5.2.0 发布记录

    github.com

  3. 3
    MuJoCo 3.15.0 发布记录

    github.com

  4. 4
    marimo 0.25.1 发布记录

    github.com

  5. 5
    StringZilla README

    github.com

  6. 6
    PyPI: stringzilla

    pypi.org

  7. 7
    StringZilla C/C++ 参考

    github.com

  8. 8
    MuJoCo README

    github.com

  9. 9
    MuJoCo Changelog

    mujoco.readthedocs.io

  10. 10
    MuJoCo Programming 指南

    mujoco.readthedocs.io

  11. 11
    MuJoCo 模型目录

    github.com

  12. 12
    marimo README

    github.com

  13. 13
    marimo 0.25.0 → 0.25.1 完整改动

    github.com

  14. 14
    marimo:运行单元格

    docs.marimo.io

GitHub 日推 · 可学可玩
GitHub 日推 · 可学可玩@xinrui.zhao

This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.

Related content

  • 1. StringZilla:把"用哪套指令集"这个判断只做一次
  • 为什么今天看
  • 最值得拆的机制
  • 复现难度:低
  • 最小上手路径
  • 今晚练习
  • 版本动作与限制
  • 2. MuJoCo:把模型编译成一块定尺寸内存,之后每一步都在上面跑
  • 为什么今天看
  • 最值得拆的机制
  • 复现难度:低(用)/中(读引擎)
  • 最小上手路径
  • 今晚练习
  • 版本动作与限制
  • 3. marimo:改一个单元格之前,先看清整张依赖图
  • 为什么今天看
  • 最值得拆的机制
  • 复现难度:低
  • 最小上手路径
  • 今晚练习
  • 版本动作与限制
  • 怎么选今晚的第一条路径