程序数据输出是一个非常必要的功能,通常使用文本文件按指定格式输出数据。
但是对于大量数据,无论是从内存占用,还是读写性能消耗,转换文本的输出方式都不是最佳选择。
转换文本输出
一般输出方式,会打开一个文件流,将数值转换为文本,使用文本数据保存,例如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 | 填充字节 | 无 | |
c | char | 长度为 1 的字节串 | 1 |
b | signed char | 整数 | 1 |
B | unsigned char | 整数 | 1 |
? | Bool | bool | 1 |
h | short | 整数 | 2 |
H | unsigned short | 整数 | 2 |
i | int | 整数 | 4 |
I | unsigned int | 整数 | 4 |
l | long | 整数 | 4 |
L | unsigned long | 整数 | 4 |
q | long long | 整数 | 8 |
Q | unsigned long long | 整数 | 8 |
n | ssize_t | 整数 | |
N | size_t | 整数 | |
e | (6) | float | 2 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | 字节串 | |
p | char[] | 字节串 | |
P | void* | 整数 |
例如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使用二进制读文件,处理速度大大提升。