
从 StringZilla、MuJoCo 和 marimo 出发,分别练习把指令集分派、模型编译和依赖图分析这三件最贵的准备工作各做一次,并给出今晚能跑通的最小路径。
strstr 快 3 倍、在 Apple Silicon 上快 9 倍,在 UTF-8 处理上比 ICU 快 10 到 70 倍。5argsort 窗口的堆栈走法、UBSan 下未对齐 SWAR 拷贝的告警、RISC-V GCM 内核分派前的探测。1 三天前的 v5.2.0 已经在做同一批 RISC-V 与 Pyodide 工作,并把 2 到 4 字节针的反向搜索改成了 SWAR。2include/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 这样的具体实现。7SZ_DYNAMIC_DISPATCH 默认为 0:所有公开函数都是 static 或 inline,后端在你编译时按编译器给的 target flags 选好。设成 1 会把每套后端都编进去,加载时把分派表初始化好,运行期只查一次表——共享库 stringzilla::shared 就是这么构建的。7SZ_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)。不写就按目标平台自动探测。70x0001000100010001,一次读 8 个字节、一次比 8 个偏移。这段代码就摆在 include/stringzilla/find/serial.h 里。7split、splitlines 和 utf8_graphemes()、utf8_wordbreaks() 这些范围都是懒求值的,从源缓冲区借出来走,每一步只交回一个 string_view。把一份几 MB 的文档切成上百万个词,每个词都不产生一次堆分配。7ß 能匹配 ss,返回的结果同时带偏移和命中长度——折叠会让命中段和针不一样长。7pip install stringzilla 一条命令。C/C++ 那条路更轻:核心是头文件库,把 include/ 放进搜索路径就能用,没有构建步骤;想要运行时分派才需要用 CMake 打开 STRINGZILLA_BUILD_SHARED。整个库零依赖,设 SZ_AVOID_LIBC=1 可以连 libc 都不要。7pip install stringzillaimport 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']#include <stringzilla/stringzilla.h>
sz_cptr_t hit = sz_find(haystack, haystack_length, "brown", 5); // 命中的指针,或 NULLstringzillas 包里,不在上面这一份。5include/stringzilla/find/,把 haswell.h 和 serial.h 并排读一遍:同一个函数在 AVX2 和 SWAR 两条路上分别长什么样。7find/serial.h 里 sz_find_2byte_serial_ 的那段循环:广播针、循环步长 8、八个偏移一起比。看懂这一段,SWAR 就懂了。7STRINGZILLA_BUILD_SHARED 构建一次,再关掉构建一次,对比两次产物里有哪些符号从 static 变成了运行期查表项。7SZ_DYNAMIC_DISPATCH 时你拿到的就是编译器 target flags 允许的那一套;而任意一个现代编译器都能编过的,正是那条标量和 SWAR 的基线。7 第三,功能在各语言绑定里不齐:Small String 只有 C/C++/C#/Java 有,排序和集合求交在 Python 和 Rust 里有、在 JS/Swift/Go 里还标着"考虑中"。5 README 那些倍数的测法写在作者自己的机器上,文档里也写明"当第一印象,不当基准套件"。5dampratio 改成按 mjModel.qpos0 处的操作空间惯量计算,多连杆上的驱动器可能因此阻尼变小;已废弃的 flex internal 碰撞选项被移除。同一版新增了 capsule 几何体的单次 multicontact、insidesite 传感器的 enclosed 属性,以及超过 2 GiB 模型的二进制序列化支持。9mjCModel;编译器(C++)把 mjCModel 变成运行期的 mjModel;引擎(C)负责所有运行期计算。10mjModel 是模型的运行期表示,mjData 是所有计算读写的工作区。README 的原话是:运行期仿真模块只操作那些由内置 XML 编译器预先分配好的底层数据结构。8m.nq 是 7、m.nv 是 6、m.nbody 是 2。mj_step 执行时不分配内存、不查名字、不碰字符串。8mjs_* 那组函数在运行期改模型、再编译。10implicitfast 是常用选择;3.13.0 加的 discrete 把约束求解和隐式速度更新合成一步,被动弹簧和执行器位置增益在远大于显式稳定极限的步长下也能稳住。9pip install mujoco 一条命令,Python 3.10 以上,wheel 里自带一份 MuJoCo。8 使用者只需要认识两个结构体:只读的 mjModel 和每步被改写的工作区 mjData。10 读源码的门槛在体量——仓库有 5,832 次提交——但 src/engine/ 是按阶段分文件的,可以顺着 mj_step 的调用顺序一层层读。<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.9282integrator 换成 euler,或者把 timestep 调到 0.02,看同一只球什么时候开始发散。这一步走通,显式积分和隐式积分的差别就不用背了。9d.qpos 和 d.qvel,再往模型里加一个 <actuator> 重新编译,看 m.nu 从 0 变成 1。你会顺带看到"改了模型就必须重新编译"这件事。10src/ 里逐个找过去,找到编译那一步的入口。10dampratio 的多连杆模型。9 第三,官方建议直接用预编译二进制,因为 main 分支顶端不一定稳定。8marimo cache size、marimo cache dir 两个命令。对"一次算好、以后复用"这件事来说,这是工具层最直接的落点。4 想看清整个 0.25 系列的改动,发布页给的对比链接从 0.25.0 一直排到 0.25.1。13my_list.append(42) 或者 foo.bar = 10 不会触发别的单元格重跑;官方给的理由是"在 Python 里可靠地跟踪可变对象做不到",建议把修改放在定义它的那个单元格里,或者干脆新建一个变量。14import 进来的名字也算全局变量。14pip 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()marimo edit 里把 n = 5 改成 20,看第三个单元格自己重跑——你没有点它。14n = 99,看它当场报"全局变量重复定义"的错。这条规则是 marimo 能保证一致性的前提。14del。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 行 XML | mj_step 把一只球从 2 米带到 1.93 米 | 需要 CPU 支持 AVX;每月发版且带破坏性变更。(README) |
| 静态依赖分析与增量重算 | marimo · 0.25.1 北京时间 10 月 2 日凌晨发布 | 一条 pip install | 改一个单元格,依赖它的那几个自己重跑 | 不跟踪对象内部变更;全局变量名必须唯一。(文档) |
github.com
github.com
github.com
github.com
github.com
pypi.org
github.com
github.com
mujoco.readthedocs.io
mujoco.readthedocs.io
github.com
github.com
github.com
docs.marimo.io
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.