在一个版图集成与分析工具的项目中看到了 一种 C++ 内存映射的用法,觉得非常强,分享一下大致的概念。
随着制造工艺的不断进步,芯片版图文件越来越大,对于一些很大的文件,可能光是打开就需要几个小时,是芯片设计开发人员的一大痛点。
于是我们领导想出了一个解决这个问题的办法:加载文件慢,是因为要在加载文件的过程中进行 parse, 把版图文件格式转换成自定义的数据格式,才能在工具中进行各种处理。采用内存映射的办法,只需要在第一次加载的时候做一次 parse,之后把自己管理的内存中的内容直接以 bit 的形式保存下来,下次加载的时候直接通过 reinterpret_cast 强制类型转换,把特定的 bit 位转换为对应的指针,就可以恢复 parse 之后的数据了,不需要再一次进行 parse,大大降低了打开文件的时间。
使用分页方式管理程序的内存可以提高内存访问效率和节约存储空间。你可以将程序的内存划分为固定大小的页面,并按需保存或加载这些页面。通过指针类型转换,可以在加载时快速恢复页面的内存结构。
在 C++ 中,一旦对象被创建,其大小通常是固定的,不会随着对象的使用而改变。对象的大小是在编译时确定的,它取决于类的成员变量和对齐规则。
当你创建一个对象时,编译器会根据类的定义计算出对象所需的内存大小,并为对象分配足够的内存空间。这个大小在对象的生命周期中保持不变,除非你对对象进行了显式的重新分配或重新定义。
需要注意的是,如果类中包含指针成员或动态分配的资源(如堆内存),对象的大小不会包括指针所指向的内存或动态分配的资源的大小。指针本身的大小是固定的,它保存了地址值而不是实际的数据。
此外,编译器可能会对对象进行对齐以满足特定的对齐规则,这可能导致对象的大小增加。对齐规则可以保证访问对象的效率,并避免因对齐不当而引起的性能问题。
总之,在一般情况下,一旦对象被创建,其大小是固定的,并且不会随着对象的使用而改变。对象的大小取决于类的成员变量和对齐规则,并且不包括指针所指向的内存或动态分配的资源的大小。
this 指针指向的地址是当前对象的内存地址。每个对象在内存中都有其自己的存储空间,包含了类的成员变量的值。this 指针指向这个存储空间的起始地址,使得成员函数可以通过 this 指针来访问对象的成员。
只要知道对象存放的首地址,就可以强制转换成对象的指针,然后通过指针访问对象
例:
class A {
public:
A(string s1) : _s1(s1) {}
long long show() { return reinterpret_cast<long long>(this); }
void s1() { cout << "s1: " << _s1 << endl; }
private:
string _s1;
};
int main() {
A a("I'm a");
long long addr = a.show();
cout << "address: " << addr << endl;
A* b = reinterpret_cast<A*>(addr); // 根据首地址获取对象指针
b->s1();
return 0;
}
/*********输出********************/
address: 247805771088
s1: I'm a
自己申请内存,在申请的内存上创建对象;通过对象的首地址获取对象
例:
class A {
public:
A(string s1) : _s1(s1) {}
long long show() { return reinterpret_cast<long long>(this); }
void s1() { cout << "s1: " << _s1 << endl; }
private:
string _s1;
};
int main() {
unsigned pagesize = 1 << 13; // 8k
char* page = new char[pagesize]; // 申请了 8k 的内存;
cout << "page: " << (void*)page << endl;
A* a = new (page) A("I'm a"); // 在申请的内存上创建对象
cout << "a: " << a << endl;
A* b = reinterpret_cast<A*>(page); //直接通过地址获取对象
b->s1();
size_t shift = sizeof(A);
page += shift; // 计算出对象 a 占用的内存
A* c = new (page) A("I'm c"); // 在未被占用的内存上创建 c 对象
A* d = reinterpret_cast<A*>(page); // 直接通过地址获取对象
d->s1();
return 0;
}
/********* 输出 **************/
page: 0x159ac205c50
a: 0x159ac205c50
s1: I'm a
s1: I'm c
上面讲过:对象的大小不会包括指针所指向的内存或动态分配的资源的大小,所以保存成文件的对象里不能有动态分配的内存(其实可以有,只是不能直接使用,要先初始化,申请内存,不然就会访问到不合法的内存),所以不能用 string,这里用 int 做成员变量举例。用有虚函数的类也比较麻烦,子类的大小难以计算。
例:
class A {
public:
A(int num) : _num(num) {}
void show() { cout << "num: " << _num << endl; }
private:
int _num;
};
void saveMemoryToFile(const char* memory, size_t size, const string& filename) {
ofstream file(filename, ios::binary);
if (!file) {
cerr << "Failed to open file for writing: " << filename << endl;
return;
}
file.write(memory, size);
file.close();
}
void readMemoryFromFile(char* memory, const string& filename) {
ifstream file(filename, ios::binary);
if (!file) {
cerr << "Failed to open file for reading: " << filename << endl;
return;
}
// 获取文件大小
file.seekg(0, ios::end);
size_t fileSize = file.tellg();
file.seekg(0, ios::beg);
// 读取文件内容
file.read(memory, fileSize);
file.close();
}
// 打印一段内存的内容,二进制表示,用于测试
void printMemoryBinary(const char* memory, std::size_t size) {
for (std::size_t i = 0; i < size; ++i) {
std::bitset<8> bits(memory[i]);
std::cout << bits << ' ';
}
std::cout << std::endl;
}
int main() {
unsigned pagesize = 1 << 13; // 8k
char* page = new char[pagesize]; // 申请了 8k 的内存;
size_t shift = sizeof(A);
char* offset = page + shift; // 第二个 A 对象的起始地址
#if 1
A* a = new (page) A(1160); // 在申请的内存上创建对象
a->show();
A* c = new (offset) A(5678); // 在未被占用的内存上创建 c 对象
c->show();
saveMemoryToFile(page, pagesize, "C:/lian/sandbox/testFile");
#else
readMemoryFromFile(page, "C:/lian/sandbox/testFile");
A* b = reinterpret_cast<A*>(page); //直接通过地址获取对象
b->show();
A* d = reinterpret_cast<A*>(offset); // 直接通过地址获取对象
d->show();
#endif
return 0;
}
把 #if 打开,执行程序。代码的作用是把类对象保存到文件里;
把 #if 关闭,打开 #else 中的代码,再执行程序。代码的作用是读取文件,直接把文件中的内容转换成类 A 的对象,
第一次输出:
num: 1160
num: 5678
第二次输出:
num: 1160
num: 5678
从结果可以看出来,成功通过访问内存的方式获取类对象。
对于比较大的数据文件,解析的时间较长,可以采用这种内存映射的方式保存出一份内存文件,这样就可以快速的打开文件,直接获取解析之后的数据格式。
博主个人网站