Есть ли способ ускорить SaveToStream на TPNGImage?


У меня есть функция, которая преобразует TBitmap (который я рисую) в TPngImage, а затем сохраняет его в stream, чтобы другие методы могли его использовать. Png используется потому, что он создает меньшие изображения для вывода отчета (excel, html). Проблема в том, что SaveToStream, кажется, занимает слишком много времени, в 15 раз больше, чем преобразование TBitmap в TPngImage или использование TStream с png. Вот код:

var
 BitmapImage: TBitmap;      
 PNGImage: TPngImage;
 PngStream: TStream;        
begin
  // draw on BitmapImage
  ...
  PNGImage := TPngImage.Create;
  PNGStream := TMemoryStream.Create;
  Try
     PNGImage.Assign(BitmapPicture.Bitmap); // Step 1: assign TBitmap to PNG
     PNGImage.SaveToStream(PNGStream);  // Step 2: save PNG to stream
     WS.Shapes.AddPicture(PNGStream,PNGImage.Width,PNGImage.Height); // Step 3: Add PNG from Stream to Excel
  finally
     PNGImage.Free;
     PNGStream.Free;
  end;
...

Это тестируется с 70000 изображений и вот тайминги:
Шаг 1: 7 s

Шаг 2: 93 s

Шаг 3: 6 s

Почему сохранение потока так медленно? Есть предложения, как это оптимизировать?

Использование Delphi XE7

EDIT

Вот пример (MCVE) с простым bmp, который преобразуется в PNG, а затем сохраняется в поток. Просто ради еще одной проверки я добавил SaveToFile, что, конечно, занимает больше времени, но это сохранение на диск, поэтому я предполагаю приемлемым.

Img1.bmp-49,5 КБ, сохраненный PNG-661 байт. ссылка на img1.формат BMP = http://www.filedropper.com/img1_1

TMemoryStreamAccess = class(TMemoryStream)
  end;

procedure TForm1.Button1Click(Sender: TObject);
var BitmapImage:TBitmap;
  PNGImage:TPngImage;
  PNGStream:TMemoryStream;//TStream;
  i,t1,t2,t3,t4,t5,t6: Integer;
  vFileName:string;
begin

  BitmapImage:=TBitmap.Create;
  BitmapImage.LoadFromFile('c:tmpimg1.bmp');

  t1:=0; t2:=0; t3:=0; t4:=0; t5:=0; t6:=0;

  for i := 1 to 70000 do
  begin

    PNGImage:=TPngImage.Create;
    PNGStream:=TMemoryStream.Create;
    try

      t1:=GetTickCount;
      PNGImage.Assign(BitmapImage);
      t2:=t2+GetTickCount-t1;

      t3:=GetTickCount;
      TMemoryStreamAccess(PNGStream).Capacity := 1000;
      PNGImage.SaveToStream(PNGStream);
      // BitmapImage.SaveToStream(PNGStream); <-- very fast!
      t4:=t4+GetTickCount-t3;

    finally
      PNGImage.Free;
      PNGstream.Free
    end;

  end;

   showmessage('Assign = '+inttostr(t2)+' - SaveToStream = '+inttostr(t4));
end;
2 6

2 ответа:

Это тестируется с 70000 изображений и вот тайминги:

Шаг 1: 7 s

Шаг 2: 93 s

Шаг 3: 6 s

Почему сохранение потока так медленно?

Давайте проверим некоторые числа:

Шаг 1: 7С = 7000ms. 7000 / 70000 = 0.1 МС на изображения

Шаг 2: 93s = 93000ms. 93000 / 70000 = ~1.33 МС на изображение Шаг 3: 6s = 6000ms. 6000 / 70000 = ~0.086 МС на изображение

Как вы думаете, 1,33 МС per SaveToStream() - это медленно? Вы просто делаете их много, так что они складываются со временем, вот и все.

При этом данные PNG в памяти не сжимаются. Он сжимается, когда данные сохраняются. Так что это одна из причин замедления. Кроме того, сохранение PNG делает много записей в поток, что может привести к тому, что поток выполнит многократное выделение памяти (re) (TPNGImage также выполняет внутреннее выделение памяти во время сохранения), так что это еще одно замедление.

Любое предложение чтобы оптимизировать это?

Вы ничего не можете сделать с накладными расходами на сжатие, но вы можете, по крайней мере, предварительно установить TMemoryStream.Capacity в разумное значение перед вызовом SaveToStream(), чтобы уменьшить перераспределение памяти, которое TMemoryStream необходимо выполнить во время записи. Вам не нужно быть точным с ним. Если запись в поток приводит к тому, что его Size превышает его текущий Capacity, он просто увеличит его Capacity соответственно. Поскольку вы уже обработали 70000 изображений, возьмите их средний размер и добавьте к нему еще несколько КБ и используйте его в качестве начального Capacity.

type
  TMemoryStreamAccess = class(TMemoryStream)
  end;

var
  BitmapImage: TBitmap;      
  PNGImage: TPngImage;
  PngStream: TMemoryStream;        
begin
  // draw on BitmapImage
  ...
  PNGImage := TPngImage.Create;
  Try
    PNGImage.Assign(BitmapPicture.Bitmap); // Step 1: assign TBitmap to PNG
    PNGStream := TMemoryStream.Create;
    try
      TMemoryStreamAccess(PNGStream).Capacity := ...; // some reasonable value
      PNGImage.SaveToStream(PNGStream);  // Step 2: save PNG to stream
      WS.Shapes.AddPicture(PNGStream, PNGImage.Width, PNGImage.Height); // Step 3: Add PNG from Stream to Excel
    finally
      PNGStream.Free;
    end;
  finally
    PNGImage.Free;
  end;
  ...

Если это все еще недостаточно быстро для вас, рассмотрите возможность использования потоков для параллельной обработки нескольких изображений. Не обрабатывайте их последовательно.

Вы назначили уровень сжатия? Я не заметил ничего похожего на

PNGImage.CompressionLevel := 1;

В вашем коде. Он может находиться в диапазоне от 0 до 9. По умолчанию - 7. Если вы установите его на 1, это будет значительно быстрее, в то время как увеличение размера выходного потока будет незначительным.