我尝试在C中为一个浮点值的3D矩阵分配一个大内存块。它的维数是44100x2200x2。这应该占用44100x2200x2x4字节的内存,大约是7.7gb。我在一台64位x86机器上使用g编译代码。当我使用htop查看过程时,我看到内存使用量增长到32GB,很快就被杀了,是不是内存计算出错了?
这是我的代码:
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
int N = 22000;
int M = 44100;
float*** a = new float**[N];
for (int m = 0; m<N; m+=1) {
cout<<((float)m/(float)N)<<endl;
a[m] = new float*[M - 1];
for (int n = 0; n<M - 1; n+=1) {
a[m][n] = new float[2];
}
}
}
编辑:我的计算不正确,我当时分配的更接近38GB。我现在修复了代码,分配15GB。
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
unsigned long N = 22000;
unsigned long M = 44100;
unsigned long blk_dim = N*(M-1)*2;
float* blk = new float[blk_dim];
unsigned long b = (unsigned long) blk;
float*** a = new float**[N];
for (int m = 0; m<N; m+=1) {
unsigned long offset1 = m*(M - 1)*2*sizeof(float);
a[m] = new float*[M - 1];
for (int n = 0; n<M - 1; n+=1) {
unsigned long offset2 = n*2*sizeof(float);
a[m][n] = (float*)(offset1 + offset2 + b);
}
}
}
3条答案
按热度按时间dbf7pr2w1#
你忘记了一个维度,以及分配内存的开销。所示的代码在第三个维度上分配内存的效率非常低,导致了太多的开销。
这将分配大约
22000 * sizeof(float **)
,大约176kb,可以忽略不计。这里的一次分配是给
44099 * sizeof(float *)
的,但是你会得到22000个这样的.22000 * 44099 * sizeof(float *)
,或者大约7.7gb的额外内存,这就是你停止计数的地方,但是你的代码还没有完成,还有很长的路要走。这是一个8字节的单次分配,但是这个分配将被完成22000 * 44099次。这是另一个*7.7gb被冲走。你现在大约需要分配超过15gig的应用程序所需的内存。
但是每个分配都不是免费的,并且
new float[2]
需要 * 超过 * 8个字节。每个单独分配的块必须由C++库在内部跟踪,以便delete
可以回收它。最简单的基于链表的堆分配实现需要一个前向指针、一个后向指针、以及分配的块中有多少字节的计数。假设不需要为对齐目的填充任何内容,则在64位平台上,每次分配至少会有24字节的开销。现在,由于第三个维分配了22000 * 44099个分配,第二个维分配了22000个分配,第一个维分配了一个分配:如果我用手指数,这将需要(22000 * 44099 + 22000 + 1) 24,或者另外22GB的内存,仅仅是为了消耗最简单、基本的内存分配方案的开销。
如果我没算错的话,我们现在使用最简单、最可能的堆分配跟踪所需的RAM高达38 GB,您的C++实现可能会使用稍微复杂一点的堆分配逻辑,但开销较大。
去掉
new float[2]
,计算矩阵的大小,new
是一个7.7gb的块,然后计算其余指针应该指向哪里,另外,为矩阵的第二维分配一块内存,计算第一维的指针。你的分配代码应该只执行三条
new
语句,一条用于第一个维度指针,一条用于第二个维度指针,还有一条用于组成第三个维度的大块数据。gt0wga4j2#
下面的示例只是对已经给出的一个答案的补充,它基本上是对这里给出的关于如何创建连续2D数组的答案的扩展,并且只说明了对
new[]
的3次调用的用法。优点是保留了通常用于三重指针的
[][][]
语法(尽管我强烈建议不要使用这样的“三星”来编写代码,但我们已经拥有了),缺点是为指针分配了更多的内存,增加了用于数据的单个内存池。Live Example
这是一个基本的RAII类。它还没有经过彻底的测试,但是应该可以正常工作。
Live Example
jgovgodb3#
这可能是问题的简化版本,但您使用的数据结构(“三星”数组)几乎从来都不是您想要的。如果您创建一个像这样的密集矩阵,并为每个元素分配空间,那么进行数百万次微小分配根本没有任何优势。如果您想要一个稀疏矩阵,通常需要压缩稀疏行这样的格式。
如果数组是“矩形”的(或者我认为3D数组是“四四方方”的),并且所有的行和列都是相同大小的,那么与分配单个内存块相比,这种数据结构纯粹是浪费,因为要执行数百万次微小的分配,为数百万个指针分配空间,并且丢失内存的局部性。
这个样板文件为动态3-D数组创建了一个零成本的抽象。同时存储底层一维
std::vector
的长度和各个维度是多余的。)API使用a(i, j, k)
作为a[i][j][k]
的等价物,使用a.at(i,j,k)
作为边界检查的变量。这个API还有一个选项,可以用索引函数
f(i,j,k)
填充数组。如果你调用a.generate(f)
,它会设置每个a(i,j,k) = f(i,j,k)
。理论上,这个强度减少了内部循环中的偏移量计算,使它更快。API还可以将生成函数作为array3d<float>(M, N, P, f)
传递给构造函数。你可以随意扩展它。值得注意的是,这段代码在技术上包含未定义的行为:它假定带符号整数乘法溢出产生负数,但实际上,如果程序在运行时请求一些不合理的存储器量,则编译器有权生成完全损坏的代码。
当然,如果数组边界是常量,只需声明它们为
constexpr
,并使用一个具有固定边界的数组。不幸的是,每一个新的C++程序员首先学习
char** argv
,因为这使人们认为“二维”数组是指向行的指针的“不规则”数组。在真实的世界中,这几乎从来都不是最适合这项工作的数据结构。