状态:讨论用草案,各章节标注置信度和待确认项 核心叙事:以资源约束维度为轴,依次呈现内存受限、CPU 并行、向量化加速、AI 接口四个维度的优化方案
约束:可用内存严格有限(数十 MB),写入数据量可达数亿行 置信度:高——现有 sec3.md 内容扎实,主要补充读侧
TSFILE_WRITER_META 单调递增,flush 不释放,close 才释放ModStat 编译期内存追踪框架(ENABLE_MEM_STAT)chunk_group_size_threshold_ 的协同(双重保险)observed_max_devices)WritingStrategy / WriteStats 数据结构write_table()、compute_strategy()(静态)状态:已确认可用 ModStat 追踪(alloc 层不区分读写,读取路径同样接入);约束接口只控制 batch_size,不控制并发列数。
读取全程内存由以下组件构成:
| 组件 | 公式 | 释放时机 |
|---|---|---|
TsBlock 输出缓冲 | $\text{batch_size} \times s_{\text{row}}$ | 每批次返回后 |
| 解压缓冲(per column) | $N_{\text{cols}} \times C_{\text{page}}$ | 每 Page 解码后 |
| Decoder / Compressor 对象 | $N_{\text{cols}} \times c_{\text{dec}}$(小量) | 查询结束 |
PageArena 临时分配 | 随查询涉及的 Chunk/Page 数增长 | 查询结束一次性回收 |
| LRU 元数据缓存 | $\leq 1000 \times \bar{b}_{\text{meta}}$(固定上限) | 进程生命周期 |
其中:
整体估算:
$$M_{\text{read}} \approx M_{\text{fixed}} + \text{batch_size} \times s_{\text{row}} + N_{\text{cols}} \times C_{\text{page}}$$
其中 $M_{\text{fixed}}$ 包含 LRU 缓存、Decoder 对象等固定开销。
关键结论:读取内存主要由列数和 batch_size 驱动,远比写侧结构简单(无元数据单调增长问题)。
实验设计:固定设备数和时间范围,变化以下两个维度:
记录各 AllocModID 的峰值内存,验证上述量化公式。
给定内存上限 $M_{\text{limit}}$ 和查询 schema,自动推导 batch_size:
$$\text{batch_size} = \left\lfloor \frac{M_{\text{limit}} - M_{\text{fixed}} - N_{\text{cols}} \times C_{\text{page}}}{s_{\text{row}}} \right\rfloor$$
接口形式:
// 纯计算:给定内存上限和 schema,返回建议的 batch_size int64_t compute_read_batch_size( int64_t mem_limit_bytes, const std::vector<ColumnSchema>& columns ); // 或作为 query() 的重载,内部自动使用推导的 batch_size reader.query(table_name, columns, start_time, end_time, result_set, filter, mem_limit_bytes);
用户不再需要手动调参,只需声明内存上限,系统保证每批次内存不超限。
注:并发解码(
ENABLE_THREADS)不引入额外内存消耗——各列解压缓冲已独立分配,并发只影响 CPU 调度,不影响内存总量。因此约束接口无需感知线程数。
约束:内存充足,目标是充分利用多核 CPU 置信度:中高——读写两侧并行模型已确认,实验数据待补充
读写两侧共享同一个并行设计哲学:
主线程负责串行的协调性工作(无法或不值得并行化的部分):
线程池负责 CPU 密集的 per-measurement 任务(完全独立,天然可并行):
并行粒度统一在测点/列层面,而非设备层面。这一设计的关键推论:
ENABLE_THREADS 关闭后完全移除线程依赖,回退到串行路径(嵌入式适配)cv_work_(唤醒空闲线程)/ cv_done_(wait_all() 同步)active_:原子递增/递减,wait_all() 等待 active_ == 0submit() + future.get() 获取结果;读侧使用 submit() + wait_all()执行模型:
主线程
│
├─ split_tablet_by_device() 串行:Tablet 按 TAG 列分组
│
├─ 对每个设备:
│ ├─ time_write_column_batch() 串行:时间列写入(共享)
│ └─ 对每个 field 列:
│ └─ pool.submit( 并行:各测点独立编码
│ value_write_column_batch(col_i))
│ └─ pool.wait_all()
│
└─ check_memory_and_may_flush()
└─ flush() 串行:单线程落盘
ValueChunkWriter 实例,编码缓冲、统计对象均不共享end_encode_chunk() → 磁盘 I/O → reset() 三步不可打断,flush 期间无新编码任务提交flush 串行瓶颈分析与双缓冲可行性:
FLUSH_CHUNK 宏将 CPU 工作(end_encode_chunk())与磁盘 I/O(flush_chunk())串行绑定,reset() 必须等 I/O 完成后才能释放 ChunkWriter 供下一批写入。这意味着 flush 期间线程池完全空闲,是潜在的吞吐瓶颈。
理论上可通过双缓冲消除此瓶颈:每个 ChunkWriter 持有 active/sealed 两套 ByteStream,flush 触发时 swap 两套缓冲,主线程立即可将新数据写入 active,后台 I/O 线程读 sealed 写盘。其可行性条件已满足(各 ChunkWriter 实例独立,ByteStream 支持 swap),工程实现不在本文范围内,作为后续优化方向。
两阶段模型:
主线程(Phase 1:串行 I/O)
│
├─ 对每个 column:
│ └─ ensure_value_page_loaded(col) 从文件读取压缩数据到内存
│
└─ 提交并行任务(Phase 2:并行 CPU)
├─ pool.submit(decompress_and_parse(col_0))
├─ pool.submit(decompress_and_parse(col_1))
│ ├─ compressor->uncompress()
│ ├─ parse null bitmap
│ └─ decoder->reset()
└─ pool.wait_all()
ValueColumnState 独立实例),无锁AlignedChunkReader::decode_cur_value_pages_multi()并发控制采用编译期 + 运行时两级门控,在 global.cc 初始化,通过 global.h 的 setter/getter 暴露:
编译期(ENABLE_THREADS)
OFF → 不引入任何线程库,完全串行,适合裸机嵌入式
ON → 线程基础设施可用,由运行时配置决定是否启用
│
▼
运行时(g_config_value_)
parallel_read_enabled_ = true 读取并行开关
parallel_write_enabled_ = true 写入并行开关
read_thread_count_ = 4 读取线程数上限(1-64)
write_thread_count_ = 6 写入线程数上限(1-64)
有效线程数 = $\min(\texttt{thread_count}, N_{\text{field}})$:配置的上限与实际任务数取小,避免空转线程的调度开销。
配置建议:
thread_count = CPU 核心数,enabled = truethread_count = field 列数,精确匹配任务数ENABLE_THREADS,运行时配置无意义固定 field 列数,对比三种工作模式:
| 模式 | 配置 | 说明 |
|---|---|---|
| 串行基线 | ENABLE_THREADS=OFF 或 enabled=false | 无线程调度开销 |
| 线程不足 | thread_count < field 列数 | 线程复用,部分并行 |
| 线程充足 | thread_count $\geq$ field 列数 | 每列一个线程,最大并行 |
分别在写入和读取两个路径上采集上述三种模式的吞吐量,field 列数取 4 / 8 / 16 作为代表场景。
约束:目标平台支持 SIMD 指令(x86 AVX2 / ARM NEON) 状态:范围已确认——时间过滤(不含值过滤);写侧差分计算和位打包待实现;批量化接口已就绪
SIMD 操作的基本要求是连续内存数组。TsFile 写入路径已构建完整的批量化接口层:
TsFileWriter::value_write_column_batch() // Tablet 列指针 → 连续数组
└── ValueChunkWriter::write_batch() // 管理 Page 边界分拆
└── ValuePageWriter::write_batch() // 全量通过 or null 散列
└── encoder->encode_batch() // 编码器批量入口 ← SIMD 发力点
读取路径同理,read_batch_int32() / read_batch_int64() 以连续数组为输出目标。
批量化的意义:逐值接口(write(t, v))在编码器内部无法形成连续数组,SIMD 无从下手;批量接口将整列数据以裸指针传入,使编码/解码核心可直接操作向量寄存器宽度对齐的内存块。
ENABLE_SIMD 关闭后完全不引入 SIMD 头文件依赖__ARM_NEON 宏检测),绕过 SIMDe 模拟层TS2DIFF 编码的三个阶段均可向量化,批量化接口使这三步直接操作连续数组:
Step 1:差分计算(待实现)
$$\delta_i = v_i - v_{i-1}, \quad i = 1, \ldots, n-1$$
相邻差是典型的向量减法:加载 $[v_1, v_2, \ldots, v_n]$ 和 $[v_0, v_1, \ldots, v_{n-1}]$,一次 _mm256_sub_epi64 得到 4 个差分值。
Step 2:Rebase(已实现)
$$\delta'i = \delta_i - \delta{\min}$$
_mm_sub_epi32(4 值/次)_mm256_sub_epi64(4 值/次)Step 3:位打包(待实现)
将 $\delta'_i$ 以 $w$ 位宽紧密打包写入字节流。SIMD 实现通过 scatter + blend 指令将多个值的位域压缩到连续字节,与解码路径的 gather 操作对称。
TS2DIFF 解码是编码的逆过程,批量解码以 4/8 值为步长:
直接从 TS2DIFF 块头推断值域,无需解码:
peek_next_block_range():读取 16-24 字节块头,计算 $[v_{\min}, v_{\max}]$skip_peeked_block():直接跳过位打包数据(advance_read_pos)satisfy_batch_time() 对连续时间戳数组应用谓词,输出布尔掩码:
_mm256_cmpgt_epi64 + movemask_pd,4 值/次vcgtq_s64,2 值/次(原生,不经 SIMDe)TimeGt、TimeGtEq、TimeLt、TimeLtEq、TimeEq、TimeNotEq、TimeBetween写入路径 读取路径
──────────────────────────────────────────────────────
encode_batch(values[], n) read_batch(out[], n)
│ │
▼ ▼
差分计算 δ[] [SIMD 待实现] 位解包 + 字节序 [SIMD 已实现]
│ │
▼ ▼
Rebase δ'[] [SIMD 已实现] 前缀和还原 [SIMD 已实现]
│ │
▼ ▼
位打包写入 [SIMD 待实现] 加基值输出 [SIMD 已实现]
│
▼ satisfy_batch_time(times[], mask[])
Page ByteStream [SIMD 已实现,仅时间过滤]
对比 ENABLE_SIMD=ON vs OFF,分别测写入编码吞吐和读取解码 + 过滤吞吐:
| 实验 | 路径 | 变量 | 指标 |
|---|---|---|---|
| 编码吞吐 | 写入 | SIMD ON/OFF × 数据类型 | M rows/s |
| 解码吞吐 | 读取 | SIMD ON/OFF × 数据类型 | M rows/s |
| 时间过滤吞吐 | 读取 | SIMD ON/OFF × 选择率 | M rows/s |
| 跨平台对比 | 读写 | x86 AVX2 vs ARM NEON vs 标量 | 加速比 |
定位:本章是前三章优化成果的应用出口。第 3-5 章分别在内存、CPU、指令集三个维度完成了底层优化;本章展示这些优化如何通过 Arrow C Data Interface 以最小开销透传到 AI/ML 生态,使底层的列式高效读取、多线程解码、SIMD 加速最终呈现在 Python DataFrame 接口上。 状态:核心实现已确认——Arrow C Data Interface 双向桥接 + Python DataFrame API,已通过测试
AI/ML 生态(pandas、PyTorch、polars 等)普遍以 Apache Arrow 为数据交换格式。TsFile 通过实现 Arrow C Data Interface(而非依赖 pyarrow C++ 库)实现了轻量的跨语言互操作。
C++ TsFile Python / pyarrow
─────────────────────────────────────────────────────
TsBlock (列式内存) pa.RecordBatch
│ ▲
│ TsBlockToArrowStruct() │ pa.RecordBatch._import_from_c(
▼ │ array_ptr, schema_ptr)
ArrowArray + ArrowSchema ──────────────┘
(Arrow C Data Interface)
│
▼
pa.RecordBatch → pd.DataFrame
设计选择:Arrow C Data Interface 是纯 C ABI 的标准(ArrowArray / ArrowSchema 结构体),不依赖 Arrow C++ 运行时,与 TsFile 的极简依赖原则一致。
TsBlockToArrowStruct() 将一个 TsBlock 转换为 Arrow struct array:
内存模型:
| 情况 | 操作 | 内存开销 |
|---|---|---|
| 非 null 数值列 | memcpy Vector ByteBuffer → Arrow buffer | 1× 数据大小 |
| 含 null 数值列 | scatter 非 null 值 + 分配 null bitmap(位反转) | 1× 数据大小 + bitmap |
| 字符串列 | 重组为 Arrow offset+data 格式 | 1× 数据大小 + offset 数组 |
| DATE 列 | YYYYMMDD → days-since-epoch 逐值转换 | 1× int32 数组 |
“单拷贝”语义:转换过程中每列数据最多经历一次 memcpy,无中间格式。pyarrow 的 _import_from_c() 接受指针所有权,本身是零拷贝。整体链路:1 次 memcpy(C++侧)+ 0 次拷贝(Python 侧)。
null bitmap 位语义差异:TsFile 内部 bitmap 以 1=null,Arrow 以 1=valid,转换时逐字节取反(~byte)。
类型映射:
| TsFile 类型 | Arrow format string | 说明 |
|---|---|---|
| INT32 | "i" | 直接映射 |
| INT64 | "l" | 直接映射 |
| TIMESTAMP | "tsn:" | 纳秒时间戳 |
| FLOAT | "f" | 直接映射 |
| DOUBLE | "g" | 直接映射 |
| BOOLEAN | "b" | 字节→位打包 |
| STRING/TEXT | "u" | offset+data 格式 |
| DATE | "tdD" | YYYYMMDD→天数转换 |
ArrowStructToTablet() 将 Arrow struct array 转换回 Tablet 供写入:
set_column_values() 直接使用 Arrow buffer 指针,有效避免额外拷贝TableSchema 参数:当已注册 schema 时,以 schema 类型为准(优先于 Arrow format 推断)基于上述 Arrow 桥接层,Python 侧提供两个核心接口:
读取:to_dataframe()
to_dataframe( file_path, table_name=None, # 默认使用文件中第一张表 column_names=None, # 列裁剪:None = 全列 start_time=None, # 时间范围过滤 end_time=None, max_row_num=None, # 行数限制(LIMIT 语义) as_iterator=False # True → Iterator[DataFrame],流式读取 )
as_iterator=True:分批 yield DataFrame,内存峰值受 batch_size 控制,适合大文件流式处理写入:write_dataframe() / dataframe_to_tsfile()
# 已有 schema 的写入 with TsFileTableWriter(path, schema) as writer: writer.write_dataframe(df) # schema 自动推断写入 dataframe_to_tsfile(df, path, table_name, time_column, tag_column)
write_dataframe 支持大小写不敏感的列名匹配dataframe_to_tsfile 从 pandas dtype 自动推断 TsFile 类型| 实验 | 变量 | 指标 |
|---|---|---|
| 端到端读取吞吐 | 列数 × batch_size | M rows/s,与 Parquet+pyarrow 对比 |
| Arrow 转换开销占比 | 行数 × 列数 | 转换耗时 / 总查询耗时 |
| 迭代器模式内存峰值 | 文件大小 × batch_size | 峰值内存 MB |
| 写入吞吐 | 行数 × 列数 | M rows/s |
| 章节 | 依赖现有工作 | 待新增实现 | 待采集实验 |
|---|---|---|---|
| 第 3 章 内存 | sec3.md 全部 + PageArena/LRU 代码 | 读侧内存分布分析 | 无(写侧已有) |
| 第 4 章 多线程 | AlignedChunkReader 并行解码、ThreadPool | 写侧并行粒度确认 | 写/读并行加速比 |
| 第 5 章 SIMD | TS2DIFF 解码、时间过滤、rebase | 值过滤/差分/位打包 SIMD(可选) | 全链路 ON vs OFF 对比 |
| 第 6 章 AI 接口 | TsBlock 设计、多级过滤、LRU | 方向待定 | 端到端 benchmark |
下一步:逐章讨论,从第 3 章开始确认内容范围和实验设计。