【问题标题】:stack overflow error in delphidelphi中的堆栈溢出错误
【发布时间】:2014-03-27 19:52:39
【问题描述】:

我有一个调用多个函数的过程:

procedure TForm1.Button1Click(Sender: TObject);
var
  rawData: TRawData;
  rawInts: TRawInts;
  processedData: TProcessedData;
begin
  rawData := getRawData();
  rawInts := getRawInts(rawData);
  processedData := getProcessedData(rawInts);
end;

数据类型定义如下:

TRawData = array[0..131069] of Byte;
TRawInts = array[0..65534] of LongInt;
TProcessedData = array[0..65534] of Double;

运行程序只需:

rawData := getRawData();
rawInts := getRawInts(rawData);

工作得很好。但是,当我尝试运行时:

getProcessedData(rawInts)

我收到堆栈溢出错误。我不明白这是为什么。 getProcessedData的函数代码很简单:

function getProcessedData(rawInts : TRawInts) : TProcessedData;
var
  i: Integer;
  tempData: TProcessedData;
  scaleFactor: Double;
begin
  scaleFactor := 0.01;

  for i := 0 to 65534 do
    tempData[i] := rawInts[i] * scaleFactor;

  Result := tempData;
end;

为什么会导致错误?

【问题讨论】:

  • 当从 getProcessedData 函数中取出代码并将其手动放入过程中时,仍然会出现此错误。除此之外,输入getRawData时似乎会抛出错误......我不知道这是为什么。
  • 局部变量在栈上分配,推测array[0..65534] of double;太大,导致栈溢出。
  • 好的,我会缩短数组,看看是否可行。
  • 你是对的。这确实有点问题,因为我真的需要双打数组那么长。我可以以某种方式延长堆栈的长度吗? (这可能是一个非常愚蠢的问题..)
  • 我可以使用全局变量来节省堆栈空间,因为我没有在函数内部创建大型数据集吗?

标签: delphi stack-overflow


【解决方案1】:

线程的默认最大堆栈大小为 1 MB。 Button1Click 的三个局部变量总共 131,070 + 65,535 * 4 + 65,535 * 8 = 917,490 字节。当您调用getProcessedData 时,您按值传递参数,这意味着该函数在堆栈上制作参数的本地副本。这增加了SizeOf(TRawInts) = 262,140 字节,使堆栈至少达到 1,179,630 字节,即大约 1.1 MB。你的堆栈溢出了。

您可以通过引用传递TRawInts 数组来减少堆栈使用。然后该功能将不会制作自己的副本。 Zdravko's answer 建议使用var,但是由于函数不需要修改传入的数组,所以应该使用const

function getProcessedData(const rawInts: TRawInts): TProcessedData;

天真地,我们可能认为getProcessedData 中的tempDataResult 变量会占用额外的堆栈空间,但实际上它们可能不会。首先,较大的返回类型通常会导致编译器更改函数签名,因此它的行为更像是使用 var 参数而不是返回值声明的函数:

procedure getProcessedData(rawInts: TRawInts; var Result: TProcessedData);

然后调用相应地进行转换:

getProcessedData(rawInts, processedData);

因此,Result 不再占用堆栈空间,因为它实际上只是调用者框架中变量的别名。

此外,有时编译器会识别出函数末尾的赋值,例如Result := tempData,意味着tempData 本身并不需要任何空间。相反,编译器可能会将您的函数视为您一直直接写入Result

begin
  scaleFactor := 0.01;

  for i := 0 to 65534 do
    Result[i] := rawInts[i] * scaleFactor;
end;

但是,最好不要指望编译器进行这些节省内存的更改。相反,最好不要一开始就过于依赖堆栈。为此,您可以使用动态数组。这些会将大量内存移出堆栈并进入 heap,这是用于动态分配的内存部分。首先更改数组类型的定义:

type
  TRawData = array of Byte;
  TRawInts = array of Integer;
  TProcessedData = array of Double;

然后,在返回这些类型的函数中,使用SetLength 分配每个数组的长度。例如,我们已经看到的函数可能是这样的:

function getProcessedData(const rawInts: TRawInts): TProcessedData;
var
  i: Integer;
  scaleFactor: Double;
begin
  Assert(Length(rawInts) = 65535);
  SetLength(Result, Length(rawInts));

  scaleFactor := 0.01;

  for i := 0 to High(rawInts) do
    Result[i] := rawInts[i] * scaleFactor;
end;

【讨论】:

  • 您好 Rob,感谢您的回答,我有一个问题,您说“由于函数不需要修改传入的数组,您应该使用 const 代替。”你是说长度吗?因为 scaleFactor 的乘法不会修改数组?
  • @Tim:不,它修改了数组的 content,而不是数组本身。他们不是一回事。传递const SL: TStringList 并不意味着您不能将项目添加到字符串列表;这意味着您不能释放字符串列表本身并在该变量中创建一个新实例。
  • 不,将两个值相乘不会修改其中任何一个值。自己看看:在调用函数之前和之后检查rawInts 的内容,你会发现该数组中的值没有改变。如果您将新值分配到您正在读取的同一数组中,则该数组不能是 const,但在您的情况下,您将产品分配到一个完全独立的数组中(tempDataResult)。
  • 这不是我指的,@Ken。当我写到使用const 而不是var 时,没有涉及到引用类型。 Delphi 不允许修改 const 静态数组参数的任何元素,是吗?
  • 我明白了,我很愚蠢,它根本没有修改 rawInts,只有结果。我现在明白了,我将更改我的代码。感谢您的宝贵时间。
【解决方案2】:

这些对象都非常大。而且您似乎将它们分配为局部变量。它们将驻留在具有固定大小的堆栈上,在 Windows 上默认为 1MB。您已经在调用堆栈的各个部分分配了足够多的这些大对象,超过了 1MB 的限制。因此堆栈溢出。

代码中的另一个问题是您将这些对象作为参数传递的方式。将大对象作为值参数传递会导致制作副本。复制一个或两个整数没什么好担心的。复制 65,000 个双打是浪费。它会损害性能。不要那样做。传递对大对象的引用。作为const 参数传递可以实现这一点。

堆栈非常适合小对象。它不适合这些大型物体。在堆上分配这些对象。使用动态数组:array of IntegerTArray<Integer> 等。

不要增加进程的默认堆栈大小。尤其是在现代多核机器中,这是内存不足错误的一个秘诀。

不要使用魔法常量。使用low()high() 获取数组边界。

请通过const 传递输入参数。这允许编译器进行非常有益的优化。

【讨论】:

  • 所以我在过程中声明我的数组,使用动态数组,那么我如何将这些传递给函数? myFunc(const myArray : double 数组) ??
  • 没错。这是一个很好的开放数组。声明为实际变量的动态数组。 TArray<T> 如果你使用现代 Delphi。
  • 好的,感谢您的耐心等待,我来自电子背景,这是我第一次使用大型数据集进行实时编码,我可以看到我对PC,我一直将其视为无限资源。
  • 我只能说不要做任何 Zdravko 建议的事情。并仔细阅读 Rob 的出色回答。
【解决方案3】:

这里的关键问题是数组的大小。

如果您使用SizeOf,您会发现它们可能比您想象的要大:

program Project3;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TRawData = array[0..131069] of Byte;
  TRawInts = array[0..65534] of Longint;
  TProcessedData = array[0..65534] of Double;
begin
  try
    writeln('TProcessedData:':20, SizeOf(TProcessedData):8);
    writeln('TRawData:':20, SizeOf(TRawData):8);
    writeln('TRawInts:':20, SizeOf(TRawInts):8);
    writeln('Total:':20, SizeOf(TRawInts) + SizeOf(TProcessedData) + SizeOf(TRawData):8);

    readln;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.

输出:

 TProcessedData:  524280
       TRawData:  131070
       TRawInts:  262140
          Total:  917490

因此,1MB 堆栈中的大部分都被数组消耗了。由于一些堆栈已经被分配,你会得到一个堆栈溢出。

这可以通过使用动态数组来避免,动态数组的内存是从中分配的。

TRawData = array of Byte;
TRawInts = array of Longint;
TProcessedData = array of Double;

...
SetLength(TProcessedData, 65535); 
...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-26
    • 2019-02-16
    • 2011-09-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多