Imageprocessor: Perf improvment from using ColorPixelAccessor instead of IPixelAccessor<Color, uint>

Created on 18 Aug 2016  路  18Comments  路  Source: JimBobSquarePants/ImageProcessor

When I uncomment the cast the number of operation on my machine jumps:
for reads 400 => 1110, for writes 320 => 720.

C# var image = new Image(400, 400) using (var imagePixels = /* (ColorPixelAccessor) */ image.Lock()) { for (int i = 0; i < 10000000; i++) { //imagePixels[200, 200] = Color.White; var color = imagePixels[200, 200]; } }

core performance

Most helpful comment

@JimBobSquarePants

May be this?

``` C#
public sealed unsafe class PixelAccessor
{
///


/// The position of the first pixel in the bitmap.
///

private void* pixelsBase;

    public TColor this[int x, int y]
    {
        get { return Unsafe.Read<TColor>(pixelsBase + ((y * Width) + x)); }
        set { return Unsafe.Write(pixelsBase + ((y * Width) + x), value); }
    }

}
```

Unsafe is from https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/

All 18 comments

Hi @omariom Thanks for picking up on that.

I'm calling IPixelAccessor<T,TP> "The Last of the Casts". I need to figure out a class that can handle the equivalent of all these PackedVector types and return the right thing without boxing.

Generics and pointers do not go well together though so I'm finding this very difficult.

I'm stuck though at present, gonna have a look at Vector<T> for inspiration.

Any ideas?

@omariom I decided to experiment taking a different approach and have pushed the commit above to test the performance passing the IPixelAccessor<T,TP> as a generic type parameter. The results are really odd though. I seem to have, not only a slowdown but an increase in memory use also. I don't know why.

Before

// * Summary *

Host Process Environment Information:
BenchmarkDotNet-Dev.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531247 ticks, Resolution=395.0622 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1586.0

Type=Resize  Mode=Throughput

                            Method |     Median |    StdDev | Scaled | Scaled-SD | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
---------------------------------- |----------- |---------- |------- |---------- |------ |------ |------ |------------------- |
             System.Drawing Resize | 64.5209 ms | 1.7151 ms |   1.00 |      0.00 |     - |     - |     - |           3,287.04 |
         ImageProcessorCore Resize | 51.9392 ms | 0.7744 ms |   0.80 |      0.02 |     - |     - | 24.75 |       5,919,250.06 |
 ImageProcessorCore Compand Resize | 73.9526 ms | 1.5449 ms |   1.14 |      0.04 |     - |     - | 25.97 |       5,874,961.18 |

// * Diagnostic Output - MemoryDiagnoser *


// ***** BenchmarkRunner: End *****

Global total time: 00:01:00 (60.14 sec)

Total time: 00:00:57 (57.43 sec)

After

// * Summary *

Host Process Environment Information:
BenchmarkDotNet-Dev.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531247 ticks, Resolution=395.0622 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1586.0

Type=Resize  Mode=Throughput

                            Method |     Median |    StdDev | Scaled | Scaled-SD | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
---------------------------------- |----------- |---------- |------- |---------- |------ |------ |------ |------------------- |
             System.Drawing Resize | 62.0691 ms | 2.2058 ms |   1.00 |      0.00 |     - |     - |     - |           3,185.17 |
         ImageProcessorCore Resize | 54.1788 ms | 0.6557 ms |   0.87 |      0.03 |     - |     - | 35.00 |       7,465,759.58 |
 ImageProcessorCore Compand Resize | 74.0705 ms | 2.1800 ms |   1.21 |      0.05 |     - |     - | 26.43 |       6,083,380.49 |

// * Diagnostic Output - MemoryDiagnoser *


// ***** BenchmarkRunner: End *****

Global total time: 00:00:58 (58.76 sec)
Press any key to continue . . .

Would you be able to have a look?

@JimBobSquarePants I think that the best way to help you is to share with you the source of my knowledge.

IMO the best book to recommend is "Pro .NET Performance" by Sasha Goldshtein (I read it at least 3 times, it's simply priceless). The 3rd chapter "Type Internals" contains very detailed and deep explanation of .NET type internals. I believe that this can give you the best understanding of cost of method calls (static vs instance vs class virtual vs struct via interface vs generic).

Then the next step would be to profile the code. My fav profiler is VS Profiler (part of VS Professional). Very good introduction to it is available here where guru of .NET debugging John Robbins gives great introduction to profiling.

The problem is that xprojs and not supported yet with VS Profiler. So you have to create a csproj and add references to the files that you want to profile (btw @omariom do you know better way of doing this?). My personal recommendation would be to go with instrumentation profilning with option "include small methods" enabled.

At this point you should have valid measurements and understanding of the perf issue.

I am sorry that I am not giving you direct answer now but I am simply too busy to check it on my own ;/

@JimBobSquarePants

May be this?

``` C#
public sealed unsafe class PixelAccessor
{
///


/// The position of the first pixel in the bitmap.
///

private void* pixelsBase;

    public TColor this[int x, int y]
    {
        get { return Unsafe.Read<TColor>(pixelsBase + ((y * Width) + x)); }
        set { return Unsafe.Write(pixelsBase + ((y * Width) + x), value); }
    }

}
```

Unsafe is from https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/

@omariom Well that's a game changer! I'd heard of this class but not seen any documentation. Just pushed a change using it plus have identified many other places I can use it to improve on Array.Copy also

Could you possible give the latest build a play with?

@adamsitnik Thanks for all that, very much appreciated. Time for me to roll my sleeves up and get learning!

@JimBobSquarePants

It will be faster if

C# private byte* pixelsBase; ... public TColor this[int x, int y] { get { return Unsafe.Read<TColor>(pixelsBase + (y * Width + x) * Unsafe.SizeOf<TColor>()); } set { Unsafe.Write(pixelsBase + (y * this.Width + x) * Unsafe.SizeOf<TColor>(), value); } }

Reads - 900, writes - 530

Instead of copying pixel one by one you can use a method like this:

``` C#
public void CopyLine(int sourceX, int sourceY, PixelAccessor target, int targetX, int targetY, int pixelCount)
{
//TODO: validate params

        byte* sourcePtr = pixelsBase + (sourceY * Width + sourceX);
        byte* targetPtr = target.pixelsBase + (targetY * target.Width + targetX);
        uint byteCount = (uint)(pixelCount * Unsafe.SizeOf<TColor>());

        Unsafe.CopyBlock(targetPtr, sourcePtr, byteCount);
        //Buffer.MemoryCopy(sourcePtr, targetPtr, byteCount, byteCount);
    }
CropProcessor and ResizeProcessor can call it instead of looping:

``` C#
sourcePixels.CopyLine(sourceX, sourceY + y, targetPixels, startX, y, endX - startX);

In my quick tests in makes crops (1024x768 => 640x480) 3 times faster.

Thanks @omariom for the update and for the extra code. I've actually been considering adding extra methods like this. BTW what are you using to measure read/write?

BTW what are you using to measure read/write?

Just a Stopwatch.

@omariom

I think there was a mistake in your tests. The source and target pointers are incorrect as they havn't been multiplied by the TColor size.

``` c#
public void CopyRow(int sourceX, int sourceY, PixelAccessor target, int targetX, int targetY, int pixelCount)
{
int size = Unsafe.SizeOf();
byte* sourcePtr = this.pixelsBase + (sourceY * this.Width + sourceX) * size;
byte* targetPtr = target.pixelsBase + (targetY * target.Width + targetX) * size;
uint byteCount = (uint)(pixelCount * size);

Unsafe.CopyBlock(targetPtr, sourcePtr, byteCount);

}

I wrote a [benchmark](https://github.com/JimBobSquarePants/ImageProcessor/blob/3ebb26faca837aeac974a67da676be409d902ae7/tests/ImageProcessorCore.Benchmarks/Image/CopyPixels.cs) comparing copying individual pixels and row and on my machine it appears individual pixels are actually faster. I _could_ of course have a mistake in my tests.

Host Process Environment Information:
BenchmarkDotNet-Dev.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531247 ticks, Resolution=395.0622 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1586.0

Type=CopyPixels Mode=Throughput

    Method |    Median |    StdDev | Gen 0 | Gen 1 |  Gen 2 | Bytes Allocated/Op |

-------------- |---------- |---------- |------ |------ |------- |------------------- |
Copy by Pixel | 2.0354 ms | 0.4523 ms | - | - | 597.00 | 2,085,263.51 |
Copy by Row | 2.5396 ms | 0.0550 ms | 8.23 | - | 742.26 | 2,583,583.83 |
```

Hmm.. Can you measure Crop itself?
Because on my machine fixed CopyLine version is still better:
Line: 0.0009 sec
Pixel: 0.0017 sec

``` C#
var image = new Image(1024,768);

Measure.Time(()=>
{
    image.Crop(640, 480);
})
.Dump();

```

Measure.Time is just a wrapper around Stopwatch.

Will do. :smile:

Sorry for the delay.

Been testing Crop itself using the two methods. And I'm still seeing a 10% slowdown using CopyRow which is surprising as I can't see how I'd possibly beat memcpy.

Per-pixel

// * Summary *

Host Process Environment Information:
BenchmarkDotNet-Dev.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531247 ticks, Resolution=395.0622 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1586.0

Type=Crop  Mode=Throughput

                  Method |        Median |        StdDev | Scaled | Scaled-SD | Gen 0 | Gen 1 |    Gen 2 | Bytes Allocated/Op |
------------------------ |-------------- |-------------- |------- |---------- |------ |------ |--------- |------------------- |
     System.Drawing Crop | 1,574.7827 us |   184.9084 us |   1.00 |      0.00 |     - |     - |        - |             146.70 |
 ImageProcessorCore Crop |   661.6304 us | 2,189.6068 us |   0.55 |      1.35 |     - |     - | 4,339.00 |       1,321,415.38 |

// * Diagnostic Output - MemoryDiagnoser *


// ***** BenchmarkRunner: End *****

Global total time: 00:02:10 (130.01 sec)

Total time: 00:01:30 (90.33 sec)

Per line

// * Summary *

Host Process Environment Information:
BenchmarkDotNet-Dev.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531247 ticks, Resolution=395.0622 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1586.0

Type=Crop  Mode=Throughput

                  Method |    Median |    StdDev | Scaled | Scaled-SD | Gen 0 | Gen 1 |    Gen 2 | Bytes Allocated/Op |
------------------------ |---------- |---------- |------- |---------- |------ |------ |--------- |------------------- |
     System.Drawing Crop | 1.5512 ms | 0.1703 ms |   1.00 |      0.00 |     - |     - |        - |             180.23 |
 ImageProcessorCore Crop | 0.8184 ms | 2.3299 ms |   0.67 |      1.46 |     - |     - | 1,336.79 |       1,284,352.94 |

// * Diagnostic Output - MemoryDiagnoser *


// ***** BenchmarkRunner: End *****

Global total time: 00:01:31 (91.68 sec)

@JimBobSquarePants Could you try calling System.Buffer.MemoryCopy instead of Unsafe.CopyBlock? It's the most optimized one available in framework itself

@adamsitnik I'll have a look. Can't benchmark properly on my home computer though so will have to wait til monday

@adamsitnik Is MemoryCopy in Core? I can't seem to find it.

@JimBobSquarePants You are right! This method is not visible from public surface for .NET CORE ;/ I am sorry, I did not know that. However I have updated your benchmarks.

What will be the most common scenario for usage of this method? I mean what size will people use? There was huge difference for huge vs small:

Host Process Environment Information:
BenchmarkDotNet-Dev.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4700MQ CPU 2.40GHz, ProcessorCount=8
Frequency=2338336 ticks, Resolution=427.6545 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1055.0

Type=ArrayCopy  Mode=Throughput  

| Method | Count | Median | StdDev | Scaled | Scaled-SD | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Copy using Array.Copy() | 100 | 30.5768 ns | 0.1895 ns | 1.00 | 0.00 | - | - | - | 0,00 |
| Copy using Unsafe | 100 | 10.0101 ns | 0.3138 ns | 0.33 | 0.01 | - | - | - | 0,00 |
| Copy using Buffer.MemoryCopy | 100 | 10.4664 ns | 0.4898 ns | 0.35 | 0.02 | - | - | - | 0,00 |
| Copy using Array.Copy() | 1000 | 41.5319 ns | 4.1919 ns | 1.00 | 0.00 | - | - | - | 0,00 |
| Copy using Unsafe | 1000 | 43.0451 ns | 2.0176 ns | 1.03 | 0.10 | - | - | - | 0,00 |
| Copy using Buffer.MemoryCopy | 1000 | 41.5373 ns | 6.3491 ns | 0.98 | 0.17 | - | - | - | 0,00 |
| Copy using Array.Copy() | 10000 | 215.7254 ns | 28.6357 ns | 1.00 | 0.00 | - | - | - | 0,06 |
| Copy using Unsafe | 10000 | 405.6742 ns | 9.6347 ns | 2.06 | 0.35 | - | - | - | 0,11 |
| Copy using Buffer.MemoryCopy | 10000 | 210.8156 ns | 30.6141 ns | 0.98 | 0.23 | - | - | - | 0,06 |

From my own personal usage of crop methods (web) it's generally a case of reducing images from 2000x1000 down to 480x240 or similar. I noticed though that switching from Array.Copy to Unsafe.BlockCopy to when copying pixel arrays of approx 1198x804 my unit tests jumped in performance.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

srdjo picture srdjo  路  6Comments

SergeBerwert picture SergeBerwert  路  8Comments

mortenbock picture mortenbock  路  3Comments

SergeBerwert picture SergeBerwert  路  4Comments

Bartmax picture Bartmax  路  8Comments