程序数据输出是一个非常必要的功能,通常使用文本文件按指定格式输出数据。

但是对于大量数据,无论是从内存占用,还是读写性能消耗,转换文本的输出方式都不是最佳选择。

转换文本输出

一般输出方式,会打开一个文件流,将数值转换为文本,使用文本数据保存,例如C++:

std::ofstream outputFile;
outputFile.open("C:\\Users\\zhenyu\\Desktop\\PlotInEarth\\GNCTEst.txt");
outputFile
    << statue.Flytime << "\t"
    << statue.PoseInEarth[0] << "\t"
    << statue.PoseInEarth[1] << "\t"
    << statue.PoseInEarth[2] << "\t"
    << quat.x() << "\t"
    << quat.y() << "\t"
    << quat.z() << "\t"
    << quat.w() << "\t\n";

1.性能问题,每一行数据需要数值转文本需要耗费时间。同时读取时文本转数值也需要时间。

2.文本保存有精度限制,扩充有效位数会极大增加空间占用。

对于有效位数为7位的数值,使用文本保存 空间占用位4 7或4 8(增加小数点)字节 每增加一位多占用1字节空间

在内存中使用float可保证7位左右的有效位数 仅占内存中的4字节 使用double可拓展到16-17位有效数值

内存数据直接输出

考虑一下结构体:

// 写出文件的结构体
struct SavedData {
    float Flytime;          // 飞行时间
    double PoseInEarthX;     // 地球坐标系X位置
    double PoseInEarthY;     // 地球坐标系Y位置
    double PoseInEarthZ;     // 地球坐标系Z位置
    float QuatX;            // 四元数X
    float QuatY;            // 四元数Y
    float QuatZ;            // 四元数Z
    float QuatW;            // 四元数W
};

通过VisualStudio内存结构查看得知其内存分布如下图:

内存分布

占内存空间为48字节,若要以文本格式输出,不考虑分隔符、换行符小数点,要达到7位有效数字则需要占用7(位有效数字) x 8(个变量) x 4(单字符占用)=224字节,远远超过内存中的占用空间大小。

若有一种可能直接将内存中的数据原封不动地写到文件中,则针对大量数据不仅可以节省占用空间,还能去除文本与数值转换的性能开销。

下面就是一个案例:

file.open("filename.bin",std::ios::out | std::ios::binary);
SaveData data{
    /*赋值代码...*/
};
file.write(reinterpret_cast<const char*>(&data), sizeof(SaveData));
file.close();

上面的代码将data强行转换成char*的指针,然后通过确定数据长度sizeof(SaveData),将指针中的数据写到文件中。

读取则按照如下格式:

file.open("filename.bin",std::ios::in | std::ios::binary);
SaveData data;
file.read(reinterpret_cast<char*>(&data), sizeof(SaveData));

因为传入的是引用,因此file可直接修改data内容,这样通过二进制将数据存放到了文本之中。

指针与vector

虽然好用,但是若结构体中出现了指针或者vector等长度动态变化的变量,伤处方法则不适用,或者说需要单独将指针解引用、将vector长度手动计算,相当不方便。因此最简单的方式就是不用,直接通过基本类型进行存写操作。

传奇Python如何做到呢

对于该问题,Python自带一个struct库来实现相关功能。

存写数据最重要的内容是确定数据的格式,字符对其与字符不对其数据格式是不一样的,Python中按如下方法进行布局说明,每个符号代表一个填充类型。

格式C 类型Python 类型标准大小
x填充字节
cchar长度为 1 的字节串1
bsigned char整数1
Bunsigned char整数1
?Boolbool1
hshort整数2
Hunsigned short整数2
iint整数4
Iunsigned int整数4
llong整数4
Lunsigned long整数4
qlong long整数8
Qunsigned long long整数8
nssize_t整数
Nsize_t整数
e(6)float2
ffloatfloat4
ddoublefloat8
schar[]字节串
pchar[]字节串
Pvoid*整数

例如C++中下面的结构体

// 写出文件的结构体
struct SavedData {
    float Flytime;          // 飞行时间
    double PoseInEarthX;     // 地球坐标系X位置
    double PoseInEarthY;     // 地球坐标系Y位置
    double PoseInEarthZ;     // 地球坐标系Z位置
    float QuatX;            // 四元数X
    float QuatY;            // 四元数Y
    float QuatZ;            // 四元数Z
    float QuatW;            // 四元数W
};

按顺序其格式说明就是为:fdddffff,针对更加复杂的结构,可参考struct---将字节串解读为打包的二进制数据)

那么Python端读取数据则为:

data = None
fmt = "fdddffff"
with open("filename.bin",'rb') as f:
    size = struct.calcsize(fmt)
    data = f.read(size)
    data = struct.unpack(fmt, data)

Python 写数据为

data = (1.0,2.0, ... ,5.6)
fmt = "fdddffff"
with open("filename.bin",'rb') as f:
    data = struct.pack(fmt, data)
    f.write(data)

边缘C#并非无法做到

针对同样C++结构体:

// 写出文件的结构体
struct SavedData {
    float Flytime;          // 飞行时间
    double PoseInEarthX;     // 地球坐标系X位置
    double PoseInEarthY;     // 地球坐标系Y位置
    double PoseInEarthZ;     // 地球坐标系Z位置
    float QuatX;            // 四元数X
    float QuatY;            // 四元数Y
    float QuatZ;            // 四元数Z
    float QuatW;            // 四元数W
};

在C#中同样构造相同的结构体

不愧都姓C,代码都一样
struct SavedData {
    float Flytime;          
    double PoseInEarthX;     
    double PoseInEarthY;     
    double PoseInEarthZ;    
    float QuatX;           
    float QuatY;            
    float QuatZ;            
    float QuatW;            
};

class Program
{
    static void Main()
    {
        string filePath = "fileName.bin";
        MyStruct data;

        // 打开文件并读取
        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            byte[] buffer = new byte[Marshal.SizeOf(typeof(SavedData))];
            fs.Read(buffer, 0, buffer.Length);

            // 将字节数组转换为结构体
            GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            try
            {
                data = (MyStruct)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(MyStruct));
            }
            finally
            {
                handle.Free();
            }
        }
        // 输出结果
        Console.WriteLine($"Flytime: {data.Flytime}, QuatW: {data.QuatW}");
    }
}

写出文件同样可以做到:

using System;
using System.IO;
using System.Runtime.InteropServices;

//[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct SavedData
{
    public float Flytime;
    public double PoseInEarthX;
    public double PoseInEarthY;
    public double PoseInEarthZ;
    public float QuatX;
    public float QuatY;
    public float QuatZ;
    public float QuatW;
}

class Program
{
    static void Main()
    {
        // 创建一个实例
        SavedData data = new SavedData
        {
            Flytime = 10.5f,
            PoseInEarthX = 123.456,
            PoseInEarthY = 234.567,
            PoseInEarthZ = 345.678,
            QuatX = 0.1f,
            QuatY = 0.2f,
            QuatZ = 0.3f,
            QuatW = 0.4f
        };

        // 将结构体写入文件
        string filePath = "SavedData.bin";
        WriteStructToFile(filePath, data);
        Console.WriteLine("数据已写入文件!");
    }

    static void WriteStructToFile(string filePath, SavedData data)
    {
        // 获取结构体大小
        int size = Marshal.SizeOf(data);

        // 分配内存并序列化
        IntPtr buffer = Marshal.AllocHGlobal(size);
        try
        {
            Marshal.StructureToPtr(data, buffer, false);
            // 创建字节数组
            byte[] bytes = new byte[size];
            Marshal.Copy(buffer, bytes, 0, size);
            // 写入文件
            File.WriteAllBytes(filePath, bytes);
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }
}

其中//[StructLayout(LayoutKind.Sequential, Pack = 1)]要根据C++代码来看是否需要对齐。

放之四海皆可行

既然这么常用到,并且格式大差不大,开始造轮子!需求:放之四海皆可行

C++

为了对于不同类型通用,C++只能用模板来写了:

//二进制文件读写器 
#include <iostream>
#include <fstream>
#include <vector>
template <typename T>
class BinaryFileRW {
public:
    static constexpr std::ios::openmode BinaryReadMode = std::ios::in | std::ios::binary;
    static constexpr std::ios::openmode BinaryWriteMode = std::ios::out | std::ios::binary;
    bool open(const std::string& filename, std::ios::openmode mode) {
        file.open(filename, mode);
        return file.is_open();
    }
    bool openForRead(const std::string& filename) {
        return open(filename, BinaryReadMode);
    }
    bool is_open() {
        return file.is_open();
    }
    bool openForWrite(const std::string& filename) {
        return open(filename, BinaryWriteMode);
    }
    void close() {
        if (file.is_open()) {
            file.close();
        }
    }
    template <typename U>
    BinaryFileRW& operator<<(const U& data) {
        file.write(reinterpret_cast<const char*>(&data), sizeof(U));
        return *this;
    }
    template <typename U>
    bool operator>>(U& data) {
        file.read(reinterpret_cast<char*>(&data), sizeof(U));
        if (file.gcount() < sizeof(U)) { // 检查实际读取的字节数是否足够
            return false;
        }
        if (file.fail() && !file.eof()) { // 处理非 EOF 的读取失败
            std::cerr << "Error: File read failed!" << std::endl;
            return false;
        }

        return true;
    }

private:
    std::fstream file;
};

上面的代码关键在于使用模板来将可能的结构体泛写为U,这样针对不同结构都可实现内容。同时重载了<<>>运算符,和标准库靠拢。

一个文件中可能写出多个数据,直接首位相连!因为内存结构确定,数据长度也就确定了,因此直接一个接一个写,读取一个接一个读,一直读到结束。

上面的库写出为:

SavedData d;
BinaryFileRW<SavedData> dataWR;
dataWR.openForWrite("filename.bin");
dataWR << d << d << d;//直接写出三个d

读取为:

SavedData d;
BinaryFileRW<SavedData> dataWR;
dataWR.openForRead("filename.bin");
//不读到结束不会停止
while (dataReader >> d) {
    std::cout 
        << d.Flytime << "\t"
        << d.PoseInEarthX << "\t"
        << d.PoseInEarthY << "\t"
        << d.PoseInEarthZ << "\t"
        << d.QuatW << "\t"
        << d.QuatX << "\t"
        << d.QuatY << "\t"
        << d.QuatZ << "\t";
}

Python

Python代码太简单,不用写库。

C

C#代码太长,不想写库。现用现造!

测试

经过测试,某数据写出时,使用文本占用89G,使用二进制,占用29G,占用大大减小,WPF使用二进制读文件,处理速度大大提升。

最后修改:2024 年 12 月 27 日
如果觉得我的文章对你有用,请随意赞赏