当编码一个非常大的文件时,我该如何避免 EOutOfMemory 异常?

4

我正在使用带有Unicode字符串的Delphi 2009。

我试图对一个非常大的文件进行编码以将其转换为Unicode:

var
  Buffer: TBytes;
  Value: string;

Value := Encoding.GetString(Buffer);

这个方法对于一个大小为40MB的缓冲区运行良好,当缓冲区大小加倍时,返回一个80MB的Unicode字符串。

但是当我尝试使用300MB的缓冲区时,会出现EOutOfMemory异常。

这不是完全出乎意料的。但是我还是决定跟踪一下它。 它进入了System单元中的DynArraySetLength过程。 在该过程中,它访问堆并调用ReallocMem。 令我惊讶的是,它成功地分配了665,124,864字节!!!

但是在DynArraySetLength的末尾,它调用了FillChar:

  // Set the new memory to all zero bits
  FillChar((PAnsiChar(p) + elSize * oldLength)^, elSize * (newLength - oldLength), 0);

您可以从注释中看出它的作用。这个例程并不复杂,但它会引发EOutOfMemory异常。以下是System单元中的FillChar函数:

procedure _FillChar(var Dest; count: Integer; Value: Char);
{$IFDEF PUREPASCAL}
var
  I: Integer;
  P: PAnsiChar;
begin
  P := PAnsiChar(@Dest);
  for I := count-1 downto 0 do
    P[I] := Value;
end;
{$ELSE}
asm                                  // Size = 153 Bytes
        CMP   EDX, 32
        MOV   CH, CL                 // Copy Value into both Bytes of CX
        JL    @@Small
        MOV   [EAX  ], CX            // Fill First 8 Bytes
        MOV   [EAX+2], CX
        MOV   [EAX+4], CX
        MOV   [EAX+6], CX
        SUB   EDX, 16
        FLD   QWORD PTR [EAX]
        FST   QWORD PTR [EAX+EDX]    // Fill Last 16 Bytes
        FST   QWORD PTR [EAX+EDX+8]
        MOV   ECX, EAX
        AND   ECX, 7                 // 8-Byte Align Writes
        SUB   ECX, 8
        SUB   EAX, ECX
        ADD   EDX, ECX
        ADD   EAX, EDX
        NEG   EDX
@@Loop:
        FST   QWORD PTR [EAX+EDX]    // Fill 16 Bytes per Loop
        FST   QWORD PTR [EAX+EDX+8]
        ADD   EDX, 16
        JL    @@Loop
        FFREE ST(0)
        FINCSTP
        RET
        NOP
        NOP
        NOP
@@Small:
        TEST  EDX, EDX
        JLE   @@Done
        MOV   [EAX+EDX-1], CL        // Fill Last Byte
        AND   EDX, -2                // No. of Words to Fill
        NEG   EDX
        LEA   EDX, [@@SmallFill + 60 + EDX * 2]
        JMP   EDX
        NOP                          // Align Jump Destinations
        NOP
@@SmallFill:
        MOV   [EAX+28], CX
        MOV   [EAX+26], CX
        MOV   [EAX+24], CX
        MOV   [EAX+22], CX
        MOV   [EAX+20], CX
        MOV   [EAX+18], CX
        MOV   [EAX+16], CX
        MOV   [EAX+14], CX
        MOV   [EAX+12], CX
        MOV   [EAX+10], CX
        MOV   [EAX+ 8], CX
        MOV   [EAX+ 6], CX
        MOV   [EAX+ 4], CX
        MOV   [EAX+ 2], CX
        MOV   [EAX   ], CX
        RET                          // DO NOT REMOVE - This is for Alignment
@@Done:
end;
{$ENDIF}

我的内存已经被分配了,但是试图用零填充它时崩溃了。这对我来说没有意义。就我所知,内存甚至不需要被填充为零 - 这可能还浪费时间 - 因为编码语句即将填充它。

我能否以某种方式防止Delphi进行内存填充?

或者是否有其他方法可以使Delphi成功地为我分配此内存?

我的真正目标是对非常大的文件执行编码语句,因此任何可实现此目标的解决方案都将不胜感激。


结论:请参见答案中的评论。

这是一个警告,要小心调试汇编代码。确保在所有“RET”行上中断,因为我错过了FillChar例程中间的一个"RET"行,错误地得出了FillChar引起了问题的结论。谢谢Mason指出这一点。

我将不得不将输入分成块以处理非常大的文件。

4个回答

6

FillChar不会分配任何内存,所以这不是你的问题。尝试跟踪它并在RET语句处设置断点,你会看到FillChar已经完成。无论问题是什么,可能是在后续步骤中出现的。


谢谢你。是的,你说得对。在 FillChar 程序中的 RET 语句是它离开的地方,所以我在程序末尾设置的断点没能捕捉到它。然后它会到达 MemoryManager.GetMem 并发出 OutOfMemory 错误信号。我将不得不像 @Romain 所说的那样将编码分成块。你帮了我很大的忙,但是 Romain 回答了我的问题,所以我必须给他采纳的答案。 - lkessler

5
从文件中读取一块内容,编码并写入另一个文件中,重复进行。

@Romain:我最初有一段代码可以处理这个问题。但是,在你将其分解的边界处,它会非常棘手,因为你可能会拆分多字节输入字符。而且,编码例程非常快,所以不一次性完成所有操作就太浪费了。 - lkessler
1
@Ikessler - 有时候你必须在时间和空间之间做出妥协。如果你每次读取4k或更多的数据,性能应该不会太差。 - Romain Hippeau
1
...或者甚至一次处理40 MB,因为您似乎能够处理它。 - Mason Wheeler
1
要做的事情是确保它以每次处理100个字节的块为单位运作,这样调试就会变得容易,并且你可以测试边界条件,然后将其设置为非常大的值(或许是动态的)用于生产代码。 - mj2008
我不会读取“块”,我会使用流(Stream)。一个快速的Unicode流,带有readline功能,应该比300 MB的虚拟内存快得多。 - Warren P

1
一个猜测:问题可能是内存过度提交,当FillChar实际访问内存时,找不到实际给你的页面?我不知道Windows是否会过度提交内存,但我知道一些操作系统会这样做——直到您尝试使用内存才会发现它。如果是这种情况,可能会导致FillChar崩溃。

谢谢您的回复,但是FillChar并不是问题所在,正如@Mason指出的那样。 - lkessler

1

程序很擅长循环。它们不知疲倦地循环,从不抱怨。

分配大量内存需要时间。会有许多调用堆管理器的操作。您的操作系统甚至不知道是否提前拥有您所需的连续内存量。您的操作系统说:“是的,我有1 GB的空闲空间。”但是,一旦您开始使用它,您的操作系统就会说:“等等,您想要所有的内存都在一个块中吗?让我确认我是否有足够的连续内存。”如果没有,您将会收到错误信息。

如果它确实拥有这些内存,那么堆管理器仍然需要做很多工作来准备内存并将其标记为已使用。

因此,显然,分配较少的内存并简单地循环使用它是有意义的。这样可以避免计算机做很多无用功,而且完成后还需要撤消这些操作。为什么不让它只做一点点工作来设置您的内存,然后继续重复使用呢?

堆栈内存的分配速度比堆内存快得多。如果您保持内存使用量较小(默认情况下低于1 MB),编译器可能会使用堆栈内存而不是堆内存,这将使您的循环速度更快。此外,分配在寄存器中的本地变量非常快。

硬盘簇和缓存大小、CPU缓存大小等因素可以提供有关最佳块大小的提示。关键是要找到一个好的数字。我喜欢使用64 KB块。


这是一个很好的评论。我将尝试使用40 MB和1 MB作为阻塞大小,并测试看看更多的堆栈分配是否比较少的堆分配更快。 - lkessler
这个想法是在你使用内存的时候保持其分配状态,但要在栈上进行分配。如果你反复调用一个在栈上分配内存然后释放它的函数,那么你仍然会多做一些工作。通过在函数内部循环使用 for 或 while 循环重复利用内存。 - Marcus Adams

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