我有一个调用多个函数的过程:
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;
为什么会导致错误?
答案 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
中的Result
和getProcessedData
变量占用额外的堆栈空间,但实际上,它们可能会赢得。首先,大型返回类型通常会导致编译器更改函数签名,因此它更像是使用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 Integer
,TArray<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);
...