垃圾发电机速度问题

时间:2011-09-03 11:07:33

标签: delphi

我正在研究生成一个充满随机字节的文件(750 MB)。我在一个单独的线程中使用的代码如下所示:

我分配了一个大小的缓冲区,因为在磁盘上写入会消耗更多时间:

function Generate(buf:Pointer):DWORD;stdcall;
var
i:DWORD;
begin
      for i := 0 to keysize -1 do
            PByte(DWORD(buf) + i)^ := Random(256);
      Result:=0;
end;

问题是这个过程完成需要很长时间。有什么想法更快的方法?如果没有其他选择,我会尝试在汇编中实现它。

8 个答案:

答案 0 :(得分:23)

这听起来像是一个很好的练习题,所以我继续实施并行解决方案。它使用稍微超过3秒来生成750 MB文件,并在其工作期间使用超过90%的CPU。 (SSD磁盘也有帮助。在RAID0磁盘对上生成文件需要3.5秒,在较慢的512 GB磁盘上生成文件需要4秒。)

所有重用的代码都可以使用OpenBSD许可证(几乎“按照您的意愿使用”):DSiWin32GpStuffGpRandomGenOtl*

uses
  DSiWin32,
  GpStuff,
  GpRandomGen,
  OtlCommon,
  OtlCollections,
  OtlParallel;

{$R *.dfm}

procedure FillBuffer(buf: pointer; bufSize: integer; randomGen: TGpRandom);
var
  buf64: PInt64;
  buf8 : PByte;
  i    : integer;
  rnd  : int64;
begin
  buf64 := buf;
  for i := 1 to bufSize div SizeOf(int64) do begin
    buf64^ := randomGen.Rnd64;
    Inc(buf64);
  end;
  rnd := randomGen.Rnd64;
  buf8 := PByte(buf64);
  for i := 1 to bufSize mod SizeOf(int64) do begin
    buf8^ := rnd AND $FF;
    rnd := rnd SHR 8;
    Inc(buf8);
  end;
end; { FillBuffer }

procedure CreateRandomFile(fileSize: integer; output: TStream);
const
  CBlockSize = 1 * 1024 * 1024 {1 MB};
var
  buffer        : TOmniValue;
  lastBufferSize: integer;
  memStr        : TMemoryStream;
  numBuffers    : integer;
  outQueue      : IOmniBlockingCollection;
begin
  outQueue := TOmniBlockingCollection.Create;
  numBuffers := (fileSize - 1) div CBlockSize + 1;
  lastBufferSize := (fileSize - 1) mod CBlockSize + 1;
  Parallel.ForEach(1, numBuffers).NoWait
    .NumTasks(Environment.Process.Affinity.Count)
    .OnStop(
      procedure
      begin
        outQueue.CompleteAdding;
      end)
    .Initialize(
      procedure(var taskState: TOmniValue)
      begin
        taskState := TGpRandom.Create;
      end)
    .Finalize(
      procedure(const taskState: TOmniValue)
      begin
        taskState.AsObject.Free;
      end)
    .Execute(
      procedure(const value: integer; var taskState: TOmniValue)
      var
        buffer      : TMemoryStream;
        bytesToWrite: integer;
      begin
        if value = numBuffers then
          bytesToWrite := lastBufferSize
        else
          bytesToWrite := CBlockSize;
        buffer := TMemoryStream.Create;
        buffer.Size := bytesToWrite;
        FillBuffer(buffer.Memory, bytesToWrite, taskState.AsObject as TGpRandom);
        outQueue.Add(buffer);
      end);
  for buffer in outQueue do begin
    memStr := buffer.AsObject as TMemoryStream;
    output.CopyFrom(memStr, 0);
    FreeAndNil(memStr);
  end;
end;

procedure TForm43.btnRandomClick(Sender: TObject);
var
  fileStr: TFileStream;
  time   : int64;
begin
  time := DSiTimeGetTime64;
  try
    fileStr := TFileStream.Create('e:\0\random.dat', fmCreate);
    try
      CreateRandomFile(750*1024*1024, fileStr);
    finally FreeAndNil(fileStr); end;
  finally Caption := Format('Completed in %d ms', [DSiElapsedTime64(time)]); end;
end;

编辑:在这种情况下使用ForEach并不是一个非常优雅的解决方案,所以我使用Parallel.ParallelTask​​和更好的IOmniCounter增强了OmniThreadLibrary。使用SVN中的版本993(或更新版本),您可以解决此多生产者 - 单一消费者问题,如下所示。

procedure CreateRandomFile(fileSize: integer; output: TStream);
const
  CBlockSize = 1 * 1024 * 1024 {1 MB};
var
  buffer   : TOmniValue;
  memStr   : TMemoryStream;
  outQueue : IOmniBlockingCollection;
  unwritten: IOmniCounter;
begin
  outQueue := TOmniBlockingCollection.Create;
  unwritten := CreateCounter(fileSize);
  Parallel.ParallelTask.NoWait
    .NumTasks(Environment.Process.Affinity.Count)
    .OnStop(Parallel.CompleteQueue(outQueue))
    .Execute(
      procedure
      var
        buffer      : TMemoryStream;
        bytesToWrite: integer;
        randomGen   : TGpRandom;
      begin
        randomGen := TGpRandom.Create;
        try
          while unwritten.Take(CBlockSize, bytesToWrite) do begin
            buffer := TMemoryStream.Create;
            buffer.Size := bytesToWrite;
            FillBuffer(buffer.Memory, bytesToWrite, randomGen);
            outQueue.Add(buffer);
          end;
        finally FreeAndNil(randomGen); end;
      end
    );
  for buffer in outQueue do begin
    memStr := buffer.AsObject as TMemoryStream;
    output.CopyFrom(memStr, 0);
    FreeAndNil(memStr);
  end;
end;

EDIT2:关于此问题的较长篇博文:Life after 2.1: Parallel data production (Introducing Parallel.Task)

答案 1 :(得分:6)

我不知道Delphi,但可能是在Random(256)电话上浪费时间。你为什么不把伪随机的东西手工编码到

的影响
n = (n * 1103515245 + 12345) & 0xff;

n从某处开始并使用递归(例如此递归)来生成下一个n。它不是 随机,但它应该用于创建随机文件。

修改 一些思考的食物。如果您正在创建此文件,希望它不易被压缩,那么上面概述的方法并不是那么好,因为& 0xff部分。这样做更好

n = (n * 1103515245 + 12345) & 0x7fffffff;

因为0x7fffffff = 2147483647是素数。并存储n的确切较大值,并在分配时执行n % 256。我对这些常量的选择有一些好的运行,并且更喜欢它作为内置.NET替代品的熵源,因为它的速度快了很多倍,而且你很少需要真正随机或更好的伪随机数。

答案 2 :(得分:4)

问题是Random()的熵有限。如果您生成 750MiB 数据,您将只获得2^31个可能的不同字符串中的一个(因为这是RNG的周期),而不是2^(750*1024*1024*8),这将是如果发电机是完美的那样的话。这是一个巨大的差异。

简而言之,如果您使用Random(),您的数据根本不是随机的。任何人都可以从4MB样本/文件中猜出所有750MiB的数据。

你必须采取不同的方式。如果你有linux机器,请从程序中执行以下命令:

dd if=/dev/urandom of=file.img bs=1M count=750

在我的旧笔记本电脑上完成了不到半分钟。

答案 3 :(得分:3)

由于随机函数无论如何都没有良好的分布,您可以将代码减少近四倍,具体如下:

function Generate(buf: Pointer): DWORD; stdcall;
var
  i: DWORD;
  p: PInteger;
begin
  p := buf;
  for i := 0 to (keysize div 4) - 1 do begin
    p^ := Random(MaxInt);
    Inc(p);
  end;
  Result := 0;
end;

更新:我的系统上面的代码需要大约650毫秒,而原始代码大约需要3秒。

答案 4 :(得分:2)

您可以尝试RandomRange(Low(Integer), High(Integer)),看看它是否有效。这将一次生成4个字节的随机数据(请注意它已经签名,我假设整数是4个字节,但是The Integer type is an Integer whose size is not guaranteed(http://www.delphibasics.co.uk/RTL .ASP?名称=整数)。

答案 5 :(得分:2)

    var
  F: TFileStream;
  I: Cardinal;
  index: integer;

  a: array[1..10240] of Cardinal;
  IndexA: integer;

  T1: TDateTime;
begin
  T1 := Now;

  F := TFileStream.Create( 'D:\filler.fil', fmCreate);
  try
    for index := 1 to (650 * MByte) div (sizeof( A)) do begin

      for indexA := 1 to 10240 do begin
        a[ IndexA] := Random( 4294967295    );
      end;
      F.WriteBuffer( A, SizeOf( A));
    end;
  finally
    F.Free;
  end;

  ShowMessage( SecondsBetween( T1, Now));
end;

在SSD驱动器上工作3~4秒。方式更容易。

答案 6 :(得分:1)

除了做你自己的Random()函数和/或使用aditional CPU,for循环一个快速的方法是:

procedure Generate(p: pointer; size: integer);
type
  TCardinalArray = array[0..0] of cardinal;
  PCardinalArray = ^TCardinalArray;

var
  i: integer;

begin
  i := (size div 4) - 1;
  while i >= 0 do
  begin
    PCardinalArray(p)[i] := Random(MaxInt) * 2;
    Dec(i);
  end;
end;

由于不需要递增指针,因此将循环索引与TEST操作进行比较。

Unit6.pas.46: i := (size div 4) - 1;
0045209C 8BD9             mov ebx,ecx
0045209E 85DB             test ebx,ebx
004520A0 7903             jns $004520a5
004520A2 83C303           add ebx,$03
004520A5 C1FB02           sar ebx,$02
004520A8 4B               dec ebx
Unit6.pas.47: while i >= 0 do
004520A9 85DB             test ebx,ebx
004520AB 7C14             jl $004520c1
Unit6.pas.49: PCardinalArray(p)[i] := Random(MaxInt) * 2;
004520AD B8FFFFFF7F       mov eax,$7fffffff
004520B2 E8C50EFBFF       call Random
004520B7 03C0             add eax,eax
004520B9 89049E           mov [esi+ebx*4],eax
Unit6.pas.50: Dec(i);
004520BC 4B               dec ebx
Unit6.pas.47: while i >= 0 do
004520BD 85DB             test ebx,ebx
004520BF 7DEC             jnl $004520ad

当然没有太大区别,但这是......

答案 7 :(得分:0)

除了其他因素外,我在原帖中看到的主要速度问题是:

1)为每个字节运行Random。此功能适用于大多数处理。每四个字节处理将是有利的。 2)最小化循环内的计算。我将建立指针边界,然后运行while循环(inc或dec by 4),直到上限和下限之间的差值小于4,然后在剩下的时间内将inc或dec减1。我可能不会在任何一点考虑for循环。 3)我不会针对大量数据运行此操作 - 我不会同时执行750MB,因为处理该数据量的速度降低往往超过代码中的任何性能增强。

非常轻微的测试,可能有很多需要改进,但我的基本想法是:

function Generate(buf: Pointer): DWord; stdcall;
  var
    inbuf, uplimit: Cardinal;
  begin
    inbuf := Cardinal(buf);
    uplimit := inbuf + keysize - 1;
    while (uplimit - inbuf) >= 4 do
      begin
        PDWord(inbuf)^ := Random(MAXINT);
        inc(inbuf, 4);
      end;
    while inbuf <= uplimit do
      begin
        PByte(inbuf)^ := Random(256);
        inc(inbuf, 1);
      end;
    Result := 0;
  end;