Проблема скорости нежелательной генератора
Я ищу в создании файла (750 МБ), заполненного случайными байтами. Код, который я использую в отдельном потоке, выглядит следующим образом:
Я выделил буфер такого размера, поскольку запись на диске занимает больше времени:
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;
Проблема заключается в том, что для завершения процесса требуется много времени. Любые идеи для более быстрого метода? Я попытаюсь реализовать его в сборке, если нет альтернативы.
Ответы
Ответ 1
Это звучало как хорошая практика, поэтому я пошел вперед и реализовал параллельное решение. Он использует чуть более 3 секунд для генерации 750 МБ файла и использует более 90% процессора во время своей работы. (SSD-диск тоже помогает. Для генерации файла на пару дисков RAID0 потребовалось 3,5 секунды и 4 секунды для создания файла на более медленном диске емкостью 512 ГБ.)
Все повторно используемые коды доступны с лицензией OpenBSD (которая почти "используется, как вы пожелаете" ): DSiWin32, GpStuff, GpRandomGen, Otl *.
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;
EDIT: использование ForEach в этом случае было не очень элегантным решением, поэтому я расширил OmniThreadLibrary с помощью Parallel.ParallelTask и с лучшим IOmniCounter. Используя выпуск 993 (или новее) из SVN, вы можете решить эту проблему с несколькими производителями-одиночными потребителями следующим образом.
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: более длинное сообщение в блоге об этой проблеме: Жизнь после 2.1: Производство параллельных данных (Представляем Parallel.Task)
Ответ 2
Я не знаю о Delphi, но может быть тратить время на вызов Random(256)
. Почему бы вам не подписать что-то псевдослучайное по отношению к эффекту
n = (n * 1103515245 + 12345) & 0xff;
Пусть n
запустится где-нибудь и воспользуемся рекурсией, такой как этот, чтобы сгенерировать следующий n
. Это не так уж и случайный, но он должен делать для создания случайных файлов.
ИЗМЕНИТЬ
Некоторая пища для размышлений. Если вы создаете этот файл в надежде, что он не будет легко сжиматься, то описанный выше метод не так хорош, из-за части & 0xff
. Лучше тогда делать
n = (n * 1103515245 + 12345) & 0x7fffffff;
as 0x7fffffff = 2147483647
- простое число. И сохраните точное большее значение n
и выполните n % 256
при назначении. У меня были хорошие результаты с этим выбором констант, и я предпочитаю его как источник энтропии для встроенной альтернативы .NET, поскольку он во много раз быстрее, и в любом случае вам редко нужны действительно случайные или лучшие псевдослучайные числа.
Ответ 3
Проблема заключается в том, что Random()
имеет ограниченную энтропию. И если вы создадите 750MiB данных, вы получите только одну из возможных t21 возможных строк (так как это период RNG), а не 2^(750*1024*1024*8)
, что будет иметь место, если генератор был идеальным. Это огромное несоответствие.
Короче говоря, если вы используете Random(), ваши данные не являются случайными вообще. Любой может угадать все 750MiB данных из 4MB образца/фрагмента файла.
Вы должны сделать это по-другому. Если у вас есть Linux-машина, выполните эту команду из вашей программы:
dd if=/dev/urandom of=file.img bs=1M count=750
Он заканчивается менее чем через полминуты на моем старом ноутбуке.
Ответ 4
Так как функция Random не имеет никакого хорошего распределения, вы можете уменьшить свой код почти в четыре раза со следующим:
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 секунд.
Ответ 5
Вы можете попробовать RandomRange(Low(Integer), High(Integer))
и посмотреть, работает ли он. Это будет генерировать 4 байта случайных данных за раз (помните, что он подписан, и что я предполагаю, что Integer равен 4 байтам, но The Integer type is an Integer whose size is not guaranteed
(http://www.delphibasics.co.uk/RTL.asp? Name = Integer).
Ответ 6
Помимо вашей собственной функции Random() и/или использования дополнительных процессоров, для циклов быстрый подход:
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
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;
Работает через 3 ~ 4 секунды на накопителе SSD. Путь проще.
Ответ 8
За исключением других факторов, основные проблемы со скоростью, которые я вижу с кодом в исходном сообщении, следующие:
1) запуск Random для каждого байта. Эта функция учитывает большую часть обработки. Обработка каждых четырех байтов будет выгодной.
2) минимизировать вычисления в цикле. Я бы установил границы указателя, а затем запустил цикл while (inc или dec by 4), пока разница между верхней границей и нижней границей не будет меньше 4, а затем inc или dec на 1 остальную часть пути. Я, вероятно, не буду рассматривать цикл for в любой точке этого. 3) Я бы не запускал это против огромного количества данных - я бы не стал делать 750 МБ одновременно, потому что ухудшение скорости обработки такого количества данных имеет тенденцию перевесить любые улучшения производительности с помощью кода.
Очень легко протестировано и, вероятно, многое предстоит улучшить, но основная идея, которую я здесь, была здесь:
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;