Skip to content

wukan1986/npyt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NPY file format with Tail

带小尾巴的NPY文件格式

项目特点

  1. NumpyNPY文件格式兼容, 可以用原生的np.load函数加载
  2. 支持memmap模式, 可以跨进程一写多读。append
  3. 支持调整文件大小resize,但不移动数据
  4. 支持tellseekrewindread等操作
  5. 支持无限写入NPY8

安装

pip install npyt

使用

import numpy as np

from npyt import NPYT

arr = np.array([1, 2, 3, 4, 5, 6])

file = "tmp.npy"
# 创建文件
nt1 = NPYT(file).save(arr, capacity=10, end=0).load(mmap_mode="r+")
# 只读加载文件
nt2 = NPYT(file).load(mmap_mode="r")
nt3 = NPYT(file).load(mmap_mode="r")

nt1.append(arr)
print(nt2.data())

nt1.append(arr[0:1])
print(nt3.data())

项目背景

本人需要一种准实时的行情存储方式,考虑arrownp.memmap()

  1. arrow,列式存储,并不适合行情数据。因为要将新数据写入到不同的位置
  2. np.memmap(),内存映射,行式存储,适合行情数据。但还是有不足
    1. 需另行维护dtypeshape等信息
    2. 文件大小随数据量而增大,不能动态调整大小

所以如果初始时创建一个大文件,然后维护一个标记,用来记录数据的位置,就能实现数据增长了。

标记是放在同一文件,还是放在不同文件呢?

最开始是放在不同文件,这样代码实现方便,但是要维护两个文件。 后来发现np.load函数支持mmap_mode参数,可以直接加载内存映射文件,并且还是直接带了dtypeshape信息。为何不直接用呢? 只要把额外信息放在NPY文件的尾部就可以了。

额外信息

额外信息放在NPY文件的尾部,多加个5个uint64数字。

  1. start: 开始位置
  2. end: 结束位置
  3. update_timestamp: 更新时间。可处理end不变,但数据变化的情况
  4. offset: 数据区开始位置,方便其他语言快速定位并写入
  5. magic: 魔术数,用来判断是否NPYT格式文件

如何实现修改文件大小而不移动数据

NPY文件头有shape信息的字符串,例如;

(20, 3)
(200, 3)

很明显,这两个字符串的长度是不一样的。而NPYT项目修改了此部分,让字符串长度一致,例如;

(                    20, 3)
(                   200, 3)

这样就可以直接修改数据大小而不用移动数据区。

兼容性

  1. np.load可以打开NPYT.save保存的文件。NPYT.load可以打开np.save保存的文件, 取原始数据NPYT._raw()
  2. np.save保存的文件大小不可修改。NPYT.save保存的文件,可用NPYT.resize修改
  3. np.loaddtype缺失align属性。NPYT.loaddtype还原了align属性。(numpy 2.2.5)
    • array要传给numba.jit函数,函数中需要对array进行修改,由于align属性缺失,可能导致修改时出现数据复制,复制出来的对象是只读
    • numpy/numpy#28973

无限写入模式NPY8

最开始提供了RingBuffer模式。但存在不足

  1. 编写过于复杂
    • c++版读写都是单行数据读取,可以用%算法来实现环形
    • numpy版。都是多行整体写入和读取,无法用%算法来实现,导致需要写大量判断语句,逻辑复杂
  2. 无法零拷贝
    • start<end,零拷贝
    • start>end,数据分成了两段,需要拷贝

所以,NPY8模式诞生了,可以7*24写入数据

它本质是创建一个文件夹和一个.lock文件,通过.lock来维护文件夹中最新的几个NPYT文件。

  1. 写入数据时,从.lock文件尾,读取最新的NPYT文件,如果文件已满,创建新的NPYT文件,并写入.lock文件
  2. .lock文件会限定文件数量,比如8个文件,多于8个文件时,文件移出队列不再维护
  3. read时,从.lock文件头开始,读取单个文件,已读完,切换下一个文件,直到.lock文件尾
  4. tail时,从.lock文件尾开始,读取单个文件,已读完,但数量不够,继续读取上一个文件,直到.lock文件头

优化建议

  1. NPY8.tail是跨文件的,如果能取的是单个文件,就能减少拷贝

    1. 返回的是np.ndarray列表,一个个按需使用比concat后使用更好
    2. capacity_per_file设置得大一些,越大越能减少跨文件的概率(tail-1)/capacity。例如:
      • capacity=100,tail=100,则99%的概率跨文件
      • capacity=100,tail=50,则49%的概率跨文件
      • capacity=50,tail=1,则0%的概率跨文件

    tail由自己的策略所决定,根据用户能接受的概率,文件大小,记录时长来设置capacity_per_file

  2. NPY8.read一次只读取一个文件,适合遍历场景。capacity很大时。可以认为与tail功能接近

    • read(n=1000, prefetch=100) 最多返回1000+100条数据
  3. NPY8是跨文件的,适合外汇、数字货币场景。股票、期货这类,一个大NPYT文件更好用

About

NPY file format with Tail

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages