为什么当内存充足时,malloc()会失败?

17

我正在使用一台内存为128GB的服务器进行某些计算。 我需要malloc()一个大小为56120 * 56120的2D浮点数组。以下是示例代码:

int main(int argc, char const *argv[])
{
    float *ls;
    int num = 56120,i,j;
    ls = (float *)malloc((num * num)*sizeof(float));
    if(ls == NULL){
        cout << "malloc failed !!!" << endl;
        while(1);
    }
    cout << "malloc succeeded ~~~" << endl;
    return 0;
}
代码编译成功,但运行时出现“malloc failed !!!”的错误提示。我的计算表明,整个数组只需要大约11GB的内存。在开始编写代码之前,我检查了服务器,发现有110GB的可用内存。为什么会出现这种错误?
我还发现,如果将num减小到,比如40000,那么malloc就会成功。
这是否意味着malloc()可以分配的最大内存有限制?
此外,如果我改变分配的方式,直接声明一个2D浮点数组,大小如下:
int main(int argc, char const *argv[])
{
    int num = 56120,i,j;
    float ls[3149454400];
    if(ls == NULL){
        cout << "malloc failed !!!" << endl;
        while(1);
    }
    cout << "malloc succeeded ~~~" << endl;
    for(i = num - 10 ; i < num; i ++){
        for( j = num - 10; j < num ; j++){
            ls[i*num + j] = 1;
        }
    }
    for(i = num - 11 ; i < num; i ++){
        for( j = num - 11; j < num ; j++){
            cout << ls[i*num + j] << endl;
        }
    }
    return 0;
}

然后我编译并运行它,结果出现了 "Segmentation fault"

我该如何解决这个问题?


2
在栈分配的数组中,3149454400个浮点数有点太多了... - Eugene Sh.
5
可能的原因之一是分配的内存必须是连续的。如果没有这么大的连续块可用,则分配将失败。 - Some programmer dude
1
你的平台是什么?Linux 64位? - Ctx
2
你应该删除 C 标签,因为你使用了 std::coutstd::endl。这当然引出了一个问题:为什么首先要使用 malloc,为什么使用 NULL 而不是 nullptr,以及为什么在使用变量之前要声明它们。 - Christian Hackl
@ZanLynx 这里不需要强制类型转换,因为 (size_t)i * i * sizeof float 的顺序已经实现了所需的扩展。通过反转顺序 sizeof (float) * i * i 可以达到同样的效果。至少这样可以避免 int 溢出导致的未定义行为。 - chux - Reinstate Monica
显示剩余5条评论
5个回答

23

问题在于,您的计算

(num * num) * sizeof(float)

计算是以32位有符号整数完成的,num=56120的结果为

-4582051584

然后将其解释为一个非常大的size_t值

18446744069127500032

你的内存不够大,这就是为什么malloc()失败的原因。

在计算malloc()时,请将num转换为size_t类型,然后它应该按预期工作。


10
num 应该是 size_t 而不是 long - Stargateur
2
@Stargateur 为什么要这样呢?num本身不是一个大小,所以使用size_t似乎不合适。 - Ctx
7
@Ctx malloc() 函数接受一个 size_t 类型的参数,应当使用 size_t 类型。 - Stargateur
2
@Stargateur 我的意思是“将 num 保持为 long 类型,并进行结果转换”。在此程序中,num 在语义上 不是 size_t,因此将其声明为 size_t 是完全误导且错误的。 - Ctx
5
请使用数据类型long来表示num,并将(num * num) * sizeof(float)的结果转换为size_t类型。尝试在Windows上运行此代码,因为即使对于64位应用程序,long仍然是一个32位带符号值。这仍然很棘手。 - Andrew Henle
显示剩余21条评论

10

正如其他人指出的那样,在 OP 的平台上,56120*56120 超出了 int 的数学范围。这是未定义行为 (UB)。

malloc(size_t x) 接受一个 size_t 参数,最好使用至少 size_t 数学计算来计算传递给它的值。通过颠倒乘法顺序,可以实现这一点。 sizeof(float) * num 使得 num 在乘法之前至少扩展到 size_t

int num = 56120,i,j;
// ls = (float *)malloc((num * num)*sizeof(float));
ls = (float *) malloc(sizeof(float) * num * num);
尽管这可以防止UB,但从数学上讲,sizeof(float)*56120*56120仍可能超出SIZE_MAX,因此这并不能防止溢出。
代码可以事先检测潜在的溢出。
if (num < 0 || SIZE_MAX/sizeof(float)/num < num) Handle_Error();

不需要强制转换malloc()的结果。
使用引用变量的大小比按类型大小更易于编写和维护。
num == 0时,malloc(0) == NULL并不一定是内存不足的情况。
总之:

int num = 56120;
if (num < 0 || ((num > 0) && SIZE_MAX/(sizeof *ls)/num < num)) {
  Handle_Error();
}
ls = malloc(sizeof *ls * num * num);
if (ls == NULL && num != 0) {
  Handle_OOM();
}

7
int num = 56120,i,j;
ls = (float *)malloc((num * num)*sizeof(float));

num * num等于56120*56120,结果为3149454400。使用signed int存储会导致溢出并导致未定义的行为。

40000之所以可行是因为40000*40000可以用int表示。

num的类型更改为long long(甚至是unsigned int)。


5
num 应该是 size_t 类型而不是 long long 或者 unsigned int - Stargateur
2
@Stargateur 实际上,任何足够大的类型都可以很好地容纳这个数字。一个 long 或者甚至是无符号整数可以容纳那么大的数字也可以工作。 - UKMonkey
5
malloc()需要一个size_t类型的参数,使用size_t类型。不要混用类型,否则会出现类似于这个问题的困扰。 - Stargateur
2
如果num表示数组的大小,那么应该使用size_t。或者OP需要正确地进行转换。你在这里说“使用long long”,但是如果下一次值不适合long long怎么办?你建议更改num的类型,为什么不使用正确的类型呢? - Stargateur
1
从语义上讲,“num”应该表示它实际代表的内容。如果我们有一行稍后写成“float angle = (1/num) * 360.0f;”,那么这也存在类似的问题。您是否会通过说“num应该是一个浮点数”来解决这个问题?如果是这样,它需要是一个浮点数和一个size_t...唯一的解决方法是在使用时正确地进行类型转换,并且其本地类型在语义上无论以后用例如何都是有意义的。 - Mike Vine
显示剩余3条评论

6

与其他人所写的内容相反,但是对我来说,将变量num从int改为size_t允许分配内存。可能是num*num在malloc中会溢出int类型。使用56120 * 56120而不是num*num进行malloc应该会抛出溢出错误。


1
请注意,对于32位的size_t类型并使用size_t num = 56120;(num * num)*sizeof(float)仍然存在问题。该乘积会导致32位的数学计算溢出。 - chux - Reinstate Monica

2

float ls[3149454400];是一个具有自动存储类型的数组,通常分配在进程堆栈上。进程堆栈默认受限于一个比您试图推送到其中的12GB要小得多的值。因此,您观察到的分段错误是由堆栈溢出引起的,而不是由malloc引起的。


@Stargateur 当然可以。这会将它从自动存储移动到静态存储。但这仍然不是一个很好的主意... 启动代码必须将整个数组设置为零,这可能需要一些时间。 - Eugene Sh.
1
稍微偏离主题,但大多数主流操作系统可能会在静态存储中管理这个非常大的数组而不会太耗时 - 它们要么共享一个“零页”,要么有一些“已经清零的页面”的供应,或者只有在访问时才分配一个零页。尽管如此,这仍然可能不是一个好主意 - 大型分配应该始终在堆上进行。 - Mike Vine
这也不是OP正在做的事情 - 3x10^9是乘法的结果,但溢出导致了一个非常不同的请求。 - UKMonkey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接