Following my previous post, where I investigated the performance impact of loading images in .NET application, I will conclude the discussion with optimization proposals.
A quick reminder of the performance impact:
- Loading images in .NET application is much more expensive than expected with an order of magnitude overhead.
- Small images has significantly larger overhead (percentage speaking).
- Combining small images into a single large image can reduce memory consumption by an order of magnitude.
- Having empty space padding in the combined image is very wasteful.
Thus the optimization will focus on two aspects:
- Reduce the number of image instances loaded.
- Reduce the overhead of the loaded images by using larger images.
1. Caching Image instances
If 'Image' instance footprint is so expensive the obvious optimization is loading as few images as possible, specifically loading each image only once.
This may seems like a no brainer but let's consider a simple WinForms scenario:
- You add an image to the project.
- You add the image to ' Resources.resx ' via the designer.
- You create user control for some reusable UI part.
- You add ' PictureBox ' to the user control.
- Using 'PictureBox Tasks' in the designer you select image from the project resource file.
Figure 1: Using resource for PictureBox image.
This simple and very common image usage has a significant flaw, every time a new instance of the user control, and a 'PictureBox' within it, is created a new 'Image' instance will be created. So that you will have many redundant instances of the same image.
This consequence is not unique in using 'PictureBox' or the designer, the underline cause is in the way .NET framework generates code used to access resources. Basically all the resources are read into memory and each time an image is requested a new 'Image' instance is created from the resource stream1.
The only way to address this issue, as far as I know, is to manually cache the created 'Image' instances and use the same instance again and again. The simplest approach is by creating a class that exposes the ability to get image by string name, enum or create a property for each image. On first request an 'Image' instance will be created and cached so all consequent request will return the same 'Image' instance.
My proposal is to use 'Resources.resx' to add the images to the project and then create 'CachedResources' static class that will expose the same properties as the generated 'Resources' static class but will lazy init them and cache them in static fields.
For this to be practical, as you don't want manually creating this code, I have create T4 template script that generates 'CachedResources' class automatically for you by the data in 'Resources.resx'.
You can get the template and read all about it in my "Using T4 Templates for caching image resources" post.
2. Merging multiple images into a single image
The basic concept is that using 10,20 or 50 small images is much more expensive than using a single image containing all of them, then rendering specific parts of it that contain the required small image.
This optimization is a bit more complicated because it requires changes in the code that uses images as 'PictureBox' for example doesn't support showing only part of the image.
I propose two option to handle this, one is using 'ImageList' that is part of .NET framework and the second is Gallery Image Manager tool that I created.
ImageList allows creation of a single instance containing multiple images of the same size that can be used in a variety of WinForms control via the ''ImageList", "ImageKey", "ImageIndex" and "ImageAlign" properties or drawn directly using 'Draw' method (figure 2). The individual images can be accessed via their index or a string key.
The image list can be populated either via code using the 'Images' collection or using a designer that will create a resource object containing a stream of all the added images (figure 3). From performance perspective you will want to create the single resource so you won't have to create image object for each small image to add to the list.
ImageList is backed by Win32 API and has some more nice features as described here.
Figure 2: Example of ImageList usage on ListView, Label, CheckBox and Button controls.
Figure 3: Adding images to ImageList via designer.
Using Gallery Image Manager
- Using the ' ImageList ' designer required to add it on a WinForms designer that makes it harder to reuse between different controls.
- Creates two kind of images in the project, regular and list that have radically different experience working with.
- It only supports image of the same size.
- No support for multiple keys for the same image.
Additionally most of the UI in my project is rendered manually so it wasn't a problem to loose WinForms controls support.
So I decided to create a tool that will help me create single image from multiple small images and preserve the rectangle (location and size) of each individual image it contains. This approach is much more low level but gives more freedom and works great with the T4 templates caching solution described above.
The result of using the tool is a single large image (gallery image) and a generated class that allows you to get the source rectangle of each individual image (gallery image part).
The gallery image is added to the project 'Resources.resx' and cached.
Static class, generated by the tool, is added to the project that allows to get image source rectangle by string key or unique property generated for each image.
When you want to draw the image you use 'Graphics.DrawImage' specifying the gallery image and source rectangle for the image part from the static class.
The usage can be simplified by creating helper classes like 'GalleryImagePart' that holds the gallery image and the image part source rectangle in a single instance, and custom controls like 'GalleryImageControl' that can accept this instance and draw it in the same way 'PictureBox' does as you can see in 14 (figure 4).
Figure 4: Using Emoticons gallery image.
Using the tool:
The Gallery Image Manager tool has two flows: (figure 5)
- Take a collection of images to create a gallery image and a static class used to draw the image parts.
- Take the gallery image and the generated static class code to split the gallery image into the individual images it contains.