首页 · ← SuperJ 手册 中文|EN

编码规范

三条风格规则,目前都是吃过亏才学到的。它们无关品味 — 每一条都是能发布的代码与悄悄让你付出代价的代码之间的差别。这些规范既适用于人类作者,也适用于 agent 作者。

目录

  1. 无 GC 编码
  2. 优先 switch 而非 if/else if
  3. 优先提前返回以减少嵌套

1. 无 GC 编码

SuperJ 没有垃圾回收器。每个 new 都是永久的 — 对象与其拥有的 arena 同寿,默认 arena 是全局的,存活到进程退出。除非你自己动手,否则不存在短生命周期对象。热路径上的一个 new 不是"回收器会吸收的一点压力" — 它是一个带速率的泄漏。

这是与 Java 最大的不同,也是每个下游项目踩到的内存 bug 的源头。平坦内存的完整指南 — 分配阶梯、arena、local 栈分配、scratch 缓冲区手册、内存事件日志 — 见 内存管理最佳实践。本节是经验法则摘要;原因与配方读那一页。

分配阶梯 — 按顺序尝试

  1. 不分配。 大多数分配可以避免。返回原语而非包装器;填充调用者拥有的数组而非返回新的;字符串预计算一次而非每次拼接。
  2. local — 栈分配小的、可证明不逃逸的临时量。需要初始化器;不允许用在原语上(普通 int 已经是栈槽)。仅当 size 是编译期常量时才有效 — 带运行时 size 它被静默忽略,你得到一个全局 arena 分配。编译器不对 local 做逃逸检查 — 超出其作用域的 local 引用是未定义行为。
  3. 复用一个长寿 static scratch 缓冲区。 对任何由数据定尺寸的东西,这是答案。分配一次,跨每次调用、分区、查询与迭代复用。按需增长、永不收缩:

``java static double[] scratch = null; static double[] ensureScratch(int n) { if (scratch == null || scratch.length < n) scratch = new double[n]; return scratch; } ``

单线程 → 每个进程一个缓冲区是安全的。无锁、无 thread-local、无防御性拷贝。顺序契约取代锁:调用者必须在下次调用覆盖前消费返回的数据。 在定义处用注释写下那个契约。

  1. arena用于一批你想一起、以 O(1) 丢弃的作用域中间量。块在每条退出路径上被 drop — 落空、returnbreak/continue。一个缺口:进行中的异常越过帧展开而不 drop arena,因此从 arena 块中 throw 仍泄漏。逃逸是放置决定,而非编译错误 — 一个会超越其块的值被放置到一个确实超越它的区域。
  2. 全局 arena — 默认 — 仅用于真正与进程同寿的东西:预计算路径表、scratch 缓冲区自身、长寿扫描器与索引。

来之不易的规则

两道门

  1. 编译期门。 在任何性能声明前用全部四个告警标志构建:

``bash superj build --warn-alloc-in-loop --warn-string-concat-in-loop \ --warn-no-reset-in-loop --warn-scratch-length ``

干净的构建意味着五种已知泄漏模式不存在。它不捕获一切 — 标志是模式特定的。

  1. 运行时门。 比较低与高迭代次数的峰值 RSS(--iter 3--iter 200)。如果峰值 RSS 随迭代增长,循环中仍有东西在分配。平坦是通过条件,不是"小"。内存事件日志 归因增长。

为此审查

读一个 diff 时,能抓住几乎一切的问题:

2. 优先 switch 而非 if/else if

SuperJ 在 case 是密集整数时把 switch 编译为跳转表,稀疏时编译为二分搜索。一个 if/else if 链总是线性扫描。

何时用 switch

规则

示例

// 好 — 跳转表,每 case 一行(允许 enum 常量)
switch (agg.op) {
    case SUM:   return c.sumDoubleRange(start, end);
    case COUNT: return (double)(end - start);
    case MIN:   return c.minDoubleRange(start, end);
    case MAX:   return c.maxDoubleRange(start, end);
    default:    return 0.0;
}
// 差 — 线性扫描,更难读
if (agg.op == AggOp.SUM) {
    return c.sumDoubleRange(start, end);
} else if (agg.op == AggOp.COUNT) {
    return (double)(end - start);
} else if (agg.op == AggOp.MIN) {
    return c.minDoubleRange(start, end);
} else if (agg.op == AggOp.MAX) {
    return c.maxDoubleRange(start, end);
} else {
    return 0.0;
}

3. 优先提前返回以减少嵌套

深嵌套让代码更难读也更难写对。方法顶部的守卫子句比把整个 body 包进 if 更清晰、更短。

规则

示例

// 好 — 守卫子句,正常路径在缩进 1
static double eval(Table t, int symId, PIndex idx) {
    if (idx == null) return 0.0;
    if (symId < 0 || symId >= idx.groupCount()) return 0.0;

    long[] bounds = idx.runBoundaries();
    double sum = 0.0;
    for (int g = 0; g < idx.groupCount(); g++) {
        int start = (int) bounds[g * 2];
        int end   = (int) bounds[g * 2 + 1];
        sum += Kernels.sumDoubleRange(t.col(0).asDoubles(), start, end);
    }
    return sum;
}
// 差 — 深嵌套,正常路径在缩进 3
static double eval(Table t, int symId, PIndex idx) {
    if (idx != null) {
        if (symId >= 0 && symId < idx.groupCount()) {
            long[] bounds = idx.runBoundaries();
            double sum = 0.0;
            for (int g = 0; g < idx.groupCount(); g++) {
                int start = (int) bounds[g * 2];
                int end   = (int) bounds[g * 2 + 1];
                sum += Kernels.sumDoubleRange(t.col(0).asDoubles(), start, end);
            }
            return sum;
        }
    }
    return 0.0;
}

两种形式产生相同代码,但第一种更易读、更易扩展、更易审查分配纪律 — 守卫子句把"不可能"情况与热路径分开,因此审查者的注意力留在循环上。

SuperJ — manual · generated from coding-guidelines.md at pack time · Powered by superJ — this site is served by superj_web 中文|EN