7

I have a function that converts TBitmap (which I draw) to TPngImage and then saves it to stream, so other methods can use it. Png is used because it creates smaller images for report output (excel, html). The problem is that SaveToStream seems to take too much time, 15x more than converting TBitmap to TPngImage or using TStream with png. Here is the code:

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;
...

This is tested with 70000 images and here are the timings:
Step 1: 7 s

Step 2: 93 s

Step 3: 6 s

Why is saving to Stream so slow? Any suggestion to optimize this?

Using Delphi XE7

EDIT

Here is example (MCVE) with simple bmp that gets converted to PNG and then saved into stream. Just for the sake of another verification I added SaveToFile, which of course takes longer, but it is saving to disk, so I assume acceptable.

The img1.bmp is 49.5KB, saved PNG is 661 bytes. link to 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:\tmp\img1.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;
Mike Torrettinni
  • 1,816
  • 2
  • 17
  • 47
  • 1
    It would be great if you could provide an MCVE – David Heffernan Aug 06 '15 at 16:41
  • @DavidHeffernan Not sure what MCVE is...? – Mike Torrettinni Aug 06 '15 at 17:03
  • You can look it up with a web search – David Heffernan Aug 06 '15 at 17:04
  • Do you mean Minimal Complete Verifiable Example? – Mike Torrettinni Aug 06 '15 at 17:24
  • @MikeTorrettinni: Yes. [How to create a Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) – Remy Lebeau Aug 06 '15 at 17:27
  • @RemyLebeau Thank you! I understand now what MCVE is, but I can't imagine how to provide a workable example, because this is just a simple Output method in a huge process - and how to provide data for 70000 images... but I understand you guys can't help me without MCVE. Any direction on this? – Mike Torrettinni Aug 06 '15 at 17:34
  • Try to produce the same pattern with a loop operating on the same image many times. – David Heffernan Aug 06 '15 at 17:35
  • OK, I'm working on it. Will take some time. – Mike Torrettinni Aug 06 '15 at 17:47
  • I wonder how accurate GetTickCount can be in this szenario. – Uwe Raabe Aug 06 '15 at 19:56
  • It might not be best option to measure timing, but gives a good enough results to see where the most execution time goes in example, right? I use ProDelphi to measure execution timing in my application. – Mike Torrettinni Aug 06 '15 at 20:21
  • 2
    Compression is expensive. Try a comparison with ImageMagik – David Heffernan Aug 06 '15 at 21:04
  • 1
    GetTickCount is fine for long durations. I'd use TStopWatch myself. – David Heffernan Aug 06 '15 at 21:05
  • I never tried ImageMagik, but doesn't it come with dll that needs to be distributed with application? Not really an option. And thank you for the TStopWathc, will try it out. – Mike Torrettinni Aug 07 '15 at 00:02
  • What will be the final use of these 70000 PNG images? Do they need to be saved as separate files or data blocks? Or could you perhaps generate some form of `bitmap atlas` (several images stored in one image as tiles) but in PNG format? Combining several smaller bitmaps into one larger bitmap and then converting that bitmap to PNG might speed up the process a bit because during conversion time the custom color palate that needs to be generated for each PNG file would have to be created only once. Also using such approach you might get better overall compression. – SilverWarior Aug 07 '15 at 03:52
  • Use ImageMagik command line to get an idea of what perf can be achieved – David Heffernan Aug 07 '15 at 04:30
  • @SilverWarior this is part of long report that creates excel files with all these images. Between 1 and 5 images per Worksheet, up to 50 Worksheets per Excel file. so, in theory I could draw up to max images per Excel file as one bitmap (100-150 images), before I need to insert them into the file separately, so I can close the file and move on. the images are of different sizes, from 200x200, 1920x1200, up to 50,000x10, so I assume I would come to other issues operating on this huge bitmap atlas. Do you have any resource I could get examples, approaches of such concept? – Mike Torrettinni Aug 07 '15 at 09:26
  • @MikeTorrettinni Based on your latest comment I don't think my suggested approach would be of any use to you since Excel does not have capability to show part of a larger picture as a standalone picture in specific worksheet. I assumed that because you have so large number of images that you might be working on some kind of a game or some other graphical intensive application and that these images are simple graphical resources. – SilverWarior Aug 07 '15 at 13:13
  • I see, no. It is a big report that creates Excel output. Actually 70000 is just a benchmark for optimization, it can be from 500-100,000 or more images. I already reduced execution time down to 30% what it used to be, and this is one of the longest executed action that seemed like something is missing as it is just working with streams. I guess TPNGImage is not that optimized as TBitmap, because as I mentioned in one of the comments for Answer 1: BitmapImage.SaveToStream(PNGStream); it finishes in 1s for 70000 iterations! – Mike Torrettinni Aug 07 '15 at 13:46
  • @Mike Your latest comment suggests that you don't understand the differences in these two image formats. Are you aware that bitmaps are uncompressed and PNG files are compressed. Did you compare with other PNG codecs yet? – David Heffernan Aug 08 '15 at 06:49
  • @David, my assumption was that once compressed, this is it, it contains compressed data - assuming occupying certain amount of memory. And only when it needs to be displayed or converted to other formats, it is decompressed. So, `code`PNGImage.Assign(BitmapImage);`code` I assumed it compresses, converts the data and now I'm operating with just data, what they represent it shouldn't mater until I need it. That's why I'm confused why SaveToStream takes so much time, why does it matter what data it is, is just data. I guess somewhere in here I get lost. – Mike Torrettinni Aug 08 '15 at 12:06
  • 1
    The data is decompressed when loaded, recompressed when saved – David Heffernan Aug 08 '15 at 16:08
  • OK, it makes sense then.I guess this is the drawback if I want smaller image disk size. – Mike Torrettinni Aug 08 '15 at 18:07
  • You've still not evaluated the performance of the Delphi PNG code as I suggested. – David Heffernan Aug 11 '15 at 05:42
  • @David, perhaps testing different PNG codecs, libraries, or ImageMagik is very simple and easy to do for experienced programmer, is not for me - it could take me days just to figure out the basic usage, let alone something more advanced (I spent almost whole day yesterday trying to figure out how to use your TReadOnlyCachedFileStream - can't make it to read simple text file; I got TWriteCachedFileStream to work) I appreciate your help, but suggestions like this are unfortunately above my experience level and without working example that I can apply to my code, I'm pretty much 'dead in water'. – Mike Torrettinni Aug 11 '15 at 17:32

3 Answers3

7

This is tested with 70000 images and here are the timings:

Step 1: 7 s

Step 2: 93 s

Step 3: 6 s

Why is saving to Stream so slow?

Let's crunch some numbers:

Step 1: 7s = 7000ms. 7000 / 70000 = 0.1ms per image

Step 2: 93s = 93000ms. 93000 / 70000 = ~1.33ms per image

Step 3: 6s = 6000ms. 6000 / 70000 = ~0.086ms per image

Do you think 1.33 ms per SaveToStream() is slow? You are just doing a LOT of them, so they add up over time, that's all.

That being said, PNG data in memory is not compressed. It gets compressed when the data is saved. So that is one reason for slowdown. Also, saving the PNG does a lot of writes to the stream, which can cause the stream to perform multiple memory (re)allocations (TPNGImage also performs internal memory allocations during saving), so that is another slowdown.

Any suggestion to optimize this?

There is nothing you can do about the compression overhead, but you can at least pre-set the TMemoryStream.Capacity to a reasonable value before calling SaveToStream() to reduce the memory reallocations that TMemoryStream needs to perform during writing. You don't need to be exact with it. If writing to the stream causes its Size to exceed its current Capacity, it will simply increase its Capacity accordingly. Since you have already processed 70000 images, take the average size of them and add a few more KB to it, and use that as your initial 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;
  ...

If that still is not fast enough for you, consider using threads to process multiple images in parallel. Don't process them sequentially.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I thought SaveToStream should be fast as it is in memory and not like SaveToFile. Images range from 1-10KB, some may go up to 100KB. This is part of 6minute process and this SaveToStream is one of the slowest parts, with cca 93s. I saw some solutions where you can set Size of stream first ( PNGStream.Size:=x ) but I don't know what Size of PNGImage would be in my case. – Mike Torrettinni Aug 06 '15 at 17:47
  • 1
    It is in memory, but it still has to allocate memory, and depending on the sizes of the images, it may have to reallocate its memory multiple times. You can set an initial `Capacity` (don't set its `Size`, since you don't know what it is) to reduce that overhead. I have updated my answer. – Remy Lebeau Aug 06 '15 at 18:05
  • I tried your suggestion, but it doesn't seem to reduce the time. See MCVE I just added to the question. I assumed the conversion from BMP to PNG is done with Assign. – Mike Torrettinni Aug 06 '15 at 18:41
  • 1
    Your MVCE does not include the `Capacity` preallocation. And yes, `Assign()` does the conversion, but the resulting pixel data is still in memory, and uncompressed. If you apply the `Capacity` fix and are able to rule out `TMemoryStream` itself as the bottleneck, then the compression is the likely culprit. Have you tried using a real profiler, like AQTime, to dig into the timings inside of `TPngImage.SaveToStream()`? It does a lot of work internally, so any number of things could be slowing it down. – Remy Lebeau Aug 06 '15 at 18:50
  • 1
    Not that you can do anything about it at that point, since it is internal to `TPNGImage`. Unless you re-write `TPNGImage` itself, or switch to a faster PNG library. Otherwise, change your code logic to compensate for the slowness. – Remy Lebeau Aug 06 '15 at 18:52
  • I submitted the MVCE before I saw your suggestion. The changes you suggested don't speed up the process in my MVCE. – Mike Torrettinni Aug 06 '15 at 19:10
  • Interesting if I test: BitmapImage.SaveToStream(PNGStream); it finishes in 1s for 70000 iterations! – Mike Torrettinni Aug 06 '15 at 19:12
  • You did not update the MVCE here with your latest code. In any case, writing BMP data has much less overhead than writing PNG data. Even then, `TBitmap` also has an internal caching mechanism where pixel data may be saved in an internal `TStream`, in which case `TBitmap.SaveToStream()` simply copies that cached data as-is to the target `TStream` without having to re-genereate the BMP data again. `TPNGImage` does not have a similar caching mechanism for its PNG data. – Remy Lebeau Aug 06 '15 at 19:21
  • MVCE is updated with your suggestion, Remy. So, the conversion to PNG does not only take time to compress the image, but also ends up as slow end result. It would be nice to have PNG with all the whistles and bells of TBitmap (like the caching you mentioned). – Mike Torrettinni Aug 07 '15 at 09:17
2

Did you assign the compression level? I didn't notice something like

PNGImage.CompressionLevel := 1;

in your code. It can be in a range of 0 to 9. By default, it is 7. If you set it to 1, it would be significantly faster, while the output stream size increase will be negligible.

Maxim Masiutin
  • 3,991
  • 4
  • 55
  • 72
1

Maybe not directly related, but i was having an issue with Read/Write (in RAM) because i was using TMemoryStream (byte by byte).

In my case i could adapt my code to work in pure RAM with SetLength(MyEncripted,TheSize); & SetLength(MyClear,TheSize); instead of TMemoryStream.Read and TMemoryStream.Write.

What i was doing was the easy 'concept' (that can be done with pure RAM string variables) of MyEncripted[i]:=Chr(Ord(MyClear[i]) xor Ord(MyKey[i])); with a TMemoryStream.Write in Byte by Byte logic.

My measured timings:

  • Using TMemoryStream for 633 KiloBytes => 3'21"
  • Using RAM String type variables for 633 KiloBytes => Near instantaneous, less than 0.1"
  • Using TMemoryStream for 6.3 MegaBytes => 33'30"
  • Using RAM String type variables for 6.3 MegaBytes => Near instantaneous, less than 0.1"
  • Using TMemoryStream for 63 MegaBytes => 5h35'00"
  • Using RAM String type variables for 63 MegaBytes => Near instantaneous, less than 0.1"
  • Using TMemoryStream for 633 MegaBytes => 55h50'00"
  • Using RAM String type variables for 633 MegaBytes => Less than 1"

Note: I did not go further in timing since 55 hours is really huge and was enough to saw what was happening; but it looks it scales 'linear'.

TMemoryStream is too slow, so slow that something what can be done in RAM taking less than ONE second can take more than TWO days while using TMemoryStream.Write (in byte by byte logic).

So i had decided long time ago to never ever use any TMemoryStream again.

Hope this can help to understand what level of SLOW is TMemoryStream versus direct RAM String variables.

Claudio
  • 11
  • 1