跳至内容
tzf 的春季更新

tzf 的春季更新

tzf 的春季更新

距离 tzf 系列项目启动已经过去了几年。上次系统性回顾开发历史,还是 2023 年初的 tzf 的演进过程。此后项目也有一些更新和维护,但主要集中在非核心功能优化和辅助功能补充上。

到了 2026 年春天,之前几个悬而未决的重要改动陆续完成了:

  1. 引入拓扑感知机制,解决多边形简化过程中额外引入的空隙和重叠问题;
  2. 基于拓扑感知机制,开发更高效的数据分发格式,完整精度数据约 17MB,简化数据约 5.4MB;
  3. 参考 tidwall/tg 项目,引入 YStripes 索引加速。

拓扑感知机制

原始数据本质上是一组多边形。由于原始边界过于精细,数据体积很大,所以需要对多边形进行简化处理。这些多边形之间存在大量共享边界,但在之前的处理中,每个多边形都是各自独立进行 RDP 简化。这就带来了 tzf 系列项目从上线开始就存在的问题:原本完整覆盖的区域,在简化后出现了空隙,以及本不该有的多边形重叠:

See details in ringsaturn/tzf#183

See details in ringsaturn/tzf#183

解决方案几年前就已经明确:先识别共享边界,再对共享边界进行简化,最后把简化后的边界替换回两侧多边形。这样可以保证相邻多边形继续引用同一条简化后的边界,从而避免因为两侧独立简化而产生新的空隙或重叠。

但是数据量确实很大。过去几年我多次尝试手动实现这个策略,最终都失败了。各种边缘情况和复杂的策略设计不断叠加,最后都会让代码无法稳定运行。

2026 年再次尝试解决这个问题时,我借助 Claude 和 Codex 做了多轮实现、验证和重构,最终把这套策略完整实现了。大致流程可以参考下面这张策略说明图:

Made by ChatGPT

Made by ChatGPT

这个策略实现之后,也就具备了实现去年设计的新数据存储格式目标的基础。

为了维持向前兼容,新的二进制数据被拆分到了新的仓库中,用于承载下文提到的格式优化。原有的数据格式分发,也就是 tzf-rel 系列,还会继续维持一段时间,之后再准备停止运行。

既然已经可以识别共享边界,那么冗长的边界就没有必要存储两遍,只需要存储一次,再使用 polyline 进行编码压缩。

这个策略的效果非常明显。tzf 系列项目此前使用 pb 格式分发完整数据集,不做 zip 压缩大约 90MB,zip 后大约 50MB。现在共享边界只存储一次,并经过 polyline 编码压缩,完整精度数据约 17MB,再做 zip 压缩后约 10MB。完整精度数据能压缩到这个体积,我自己还是比较满意的。也正是因为这个体积已经可以接受,tzf-rs 终于开始提供可选 feature 来支持完整数据集了。在此之前,受限于 90MB 的庞大体积,完整数据集只能让用户自行下载并提供访问路径。

对于简化后的数据集,如果不做 polyline 压缩,体积还会轻微膨胀。原因是此前有很多小的多边形细节会被直接抹去,现在引入了新的判定条件,出于精度考虑保留了大量细小多边形细节。另一方面,因为边界本身已经被大幅简化,共享边界只存储一份带来的优化效果也没有完整精度数据那么显著。目前引入共享边界识别和 polyline 处理后,简化数据集大约 5.4MB,仍然可以接受。

不过这里还是要提一句,tzf 系列项目使用完整精度数据时,运行过程中需要的内存在 500MB 左右,这个占用还是很大,暂时没有进一步优化的计划,并且这个功能暂时不会下放到 Python binding 中。即使使用简化数据集,也需要约 100MB 内存。tzf 系列项目,特别是 Go、Rust、Python 三个版本,设计之初就是为了服务高并发后端 API 场景,可以接受一定的内存占用,换取几乎无感的处理耗时,同时边界精度也不能过度简化。在这个场景下,内存占用、处理速度、数据精度需要一起权衡。具体用什么、怎么用,还是要以各自的实际情况为准。

具体的功能可以参考代码的文档 internal/topology/README.md

目前的数据文件列表如下:

文件名大小说明
combined-with-oceans.compress.topo.bin~17MB完整精度:共享边界去重 + polyline 压缩
combined-with-oceans.topology.compress.topo.bin~5.4MB简化版:拓扑感知简化 + 共享边界去重 + polyline 压缩
combined-with-oceans.topology.preindex.bin~2MBFuzzyFinder 使用的瓦片预索引

YStripes 索引

首先声明,YStripes 索引不是我发明的,它来自 Josh Baker 此前发布的 tidwall/tg 项目。只是将这个索引机制移植到了 tzf 的 Go 和 Rust 版本中。

从这个春天开始,这个索引已经成为 tzf 的 Go 和 Rust 版本的默认策略。它确实增加了一些内存占用,但性能收益更明显。在我的本地 benchmark 中,单次随机查询已经降到 1 微秒左右,基本不会构成我已知使用场景中的性能瓶颈。

对应的算法原理这里就不展开了,感兴趣可以直接阅读作者的说明文档 POLYGON_INDEXING.md

Benchmark

这里简单展示一下我本地的 benchmark 结果。测试设备是 MacBook Pro with Apple M3 Max。

以下结果主要用于观察不同策略之间的相对差异,不建议直接作为跨机器的绝对性能结论。

tzf(Go)

TargetDatasetScenarioMedian (ns)p99 (ns)Approx throughput (ops/s)Memory (MiB)
DefaultFindertopology-simplified + preindexedge case · GetTimezoneName3000.03000.0393.5K74.70
Findertopology-simplifiededge case · GetTimezoneName2000.03000.0470.4K66.00
FullFinderfull-precision + preindexedge case · GetTimezoneName3000.03000.0395.6K421.50
Finderfull-precisionedge case · GetTimezoneName2000.03000.0475.3K412.70
DefaultFindertopology-simplified + preindexrandom world cities · GetTimezoneName1000.04000.01162.4K74.70
FuzzyFinderpreindexrandom world cities · GetTimezoneName469.81000.02128.6K8.90
Findertopology-simplifiedrandom world cities · GetTimezoneName2000.04000.0531.6K66.00
FullFinderfull-precision + preindexrandom world cities · GetTimezoneName1000.04000.01143.1K421.50
Finderfull-precisionrandom world cities · GetTimezoneName2000.05000.0468.6K412.70
DefaultFindertopology-simplified + preindexrandom world cities · GetTimezoneNames5000.09000.0208.0K74.70
FuzzyFinderpreindexrandom world cities · GetTimezoneNames462.71000.02161.2K8.90
Findertopology-simplifiedrandom world cities · GetTimezoneNames5000.08000.0211.5K66.00
FullFinderfull-precision + preindexrandom world cities · GetTimezoneNames5000.09000.0192.8K421.50

tzf-rs(Rust)

Topology-Simplified (bundled):

TargetDatasetScenarioMedian estimate (µs)Approx throughput (ops/s)Avg peak RSS (MiB)
Findertopology-simplifiedYStripes only1.2296813,273103.30
Findertopology-simplifiedNo index6.5402152,90151.68
DefaultFindertopology-simplified + preindexYStripes only1.1383878,503125.98
DefaultFindertopology-simplified + preindexNo index2.2514444,16877.79

Full-Precision (full):

TargetDatasetScenarioMedian estimate (µs)Approx throughput (ops/s)Avg peak RSS (MiB)
Finder (full)full-precisionYStripes only2.0852479,570561.08
Finder (full)full-precisionNo index37.698026,527252.54
DefaultFinder (full)full-precision + preindexYStripes only1.3488741,400584.30
DefaultFinder (full)full-precision + preindexNo index11.275088,692278.63

Python

Python 本身主要是 binding,这里就不贴 benchmark 结果了。不过值得一提的是,whl 体积从 7MB 左右降到了 4MB 左右,也算是对镜像构建产物的一点小优化。

Continuous Benchmark in GitHub Actions

下面是利用 Continuous Benchmark 监控的长期性能指标:

tzf ns/op

tzf ns/op

tzf-rs ns/iter

tzf-rs ns/iter

tzf iter/sec

tzf iter/sec

End

以上就是这个春天密集完成的主要功能。对 tzf 系列项目来说,这次更新也算是补上了最早设计里的关键功能:用 Go 完成拓扑感知的多边形数据集简化和分发,再让 Go、Rust、Python 等不同语言版本直接复用同一套数据结果。

后续维护工作会相对轻一些,主要集中在数据文件更新、项目依赖更新和少量接口兼容工作上。

上述的开发分散在不同时间段,对应的 release 参考:

最后更新于