delphi中的堆栈溢出错误

时间:2014-03-27 19:52:40

标签: delphi stack-overflow

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

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)

我收到stackoverflow错误。我不明白为什么会这样。 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;

为什么会导致错误?

3 个答案:

答案 0 :(得分:13)

线程的默认最大堆栈大小为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;

天真地,我们可能希望tempData中的ResultgetProcessedData变量占用额外的堆栈空间,但实际上,它们可能会赢得。首先,大型返回类型通常会导致编译器更改函数签名,因此它更像是使用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;

但是,最好不要在编译器上 count 来进行那些节省内存的更改。相反,最好不要首先在堆栈上如此重视。为此,您可以使用动态数组。这些将把大量内存移出堆栈并进入,这是用于动态分配的内存部分。首先更改数组类型的定义:

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;

答案 1 :(得分:2)

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

代码中的另一个问题是将这些对象作为参数传递的方式。将大对象作为值参数传递会导致复制。复制一两个整数无需担心。复制6.5万双打是浪费。它伤害了性能。不要这样做。传递对大对象的引用。传递const参数即可实现。

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

不要增加进程的默认堆栈大小。特别是在现代的多核机器中,这是内存错误的一种方法。

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

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

答案 2 :(得分:0)

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

如果您使用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); 
...