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];
}
}
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
{
//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
{
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
| Copy using Buffer.MemoryCopy
| Copy using Array.Copy() | 1000 | 41.5319 ns | 4.1919 ns | 1.00 | 0.00 | - | - | - | 0,00 |
| Copy using Unsafe
| Copy using Buffer.MemoryCopy
| Copy using Array.Copy() | 10000 | 215.7254 ns | 28.6357 ns | 1.00 | 0.00 | - | - | - | 0,06 |
| Copy using Unsafe
| Copy using Buffer.MemoryCopy
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.
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;
}
```
Unsafe is from https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/