安全でないポインターイテレーションとビットマップ - なぜUINT64がより速いのですか?
-
25-10-2019 - |
質問
私はいくつかの安全でないビットマップ操作を行ってきましたが、ポインターを減らすことで大きなパフォーマンスの改善につながる可能性があることがわかりました。なぜそうなのかはわかりませんが、ループでもっとビットごとに操作を行っていても、ポインターでの繰り返しを減らす方が良いです。
たとえば、UINT32を2ピクセルでUINT64で繰り返して32ピクセルを超える代わりに、1サイクルで2倍の操作を実行します。
以下は、2つのピクセルを読み取り、それらを変更することでそれを行います(もちろん、奇妙な幅の画像で失敗しますが、テスト用だけです)。
private void removeBlueWithTwoPixelIteration()
{
// think of a big image with data
Bitmap bmp = new Bitmap(15000, 15000, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
TimeSpan startTime, endTime;
unsafe {
UInt64 doublePixel;
UInt32 pixel1;
UInt32 pixel2;
const int readSize = sizeof(UInt64);
const UInt64 rightHalf = UInt32.MaxValue;
PerformanceCounter pf = new PerformanceCounter("System", "System Up Time"); pf.NextValue();
BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat);
byte* image = (byte*)bd.Scan0.ToPointer();
startTime = TimeSpan.FromSeconds(pf.NextValue());
for (byte* line = image; line < image + bd.Stride * bd.Height; line += bd.Stride)
{
for (var pointer = line; pointer < line + bd.Stride; pointer += readSize)
{
doublePixel = *((UInt64*)pointer);
pixel1 = (UInt32)(doublePixel >> (readSize * 8 / 2)) >> 8; // loose last 8 bits (Blue color)
pixel2 = (UInt32)(doublePixel & rightHalf) >> 8; // loose last 8 bits (Blue color)
*((UInt32*)pointer) = pixel1 << 8; // putback but shift so A R G get back to original positions
*((UInt32*)pointer + 1) = pixel2 << 8; // putback but shift so A R G get back to original positions
}
}
endTime = TimeSpan.FromSeconds(pf.NextValue());
bmp.UnlockBits(bd);
bmp.Dispose();
}
MessageBox.Show((endTime - startTime).TotalMilliseconds.ToString());
}
次のコードは、ピクセルごとにピクセルを行い、 約70%遅い 以前よりも:
private void removeBlueWithSinglePixelIteration()
{
// think of a big image with data
Bitmap bmp = new Bitmap(15000, 15000, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
TimeSpan startTime, endTime;
unsafe
{
UInt32 singlePixel;
const int readSize = sizeof(UInt32);
PerformanceCounter pf = new PerformanceCounter("System", "System Up Time"); pf.NextValue();
BitmapData bd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat);
byte* image = (byte*)bd.Scan0.ToPointer();
startTime = TimeSpan.FromSeconds(pf.NextValue());
for (byte* line = image; line < image + bd.Stride * bd.Height; line += bd.Stride)
{
for (var pointer = line; pointer < line + bd.Stride; pointer += readSize)
{
singlePixel = *((UInt32*)pointer) >> 8; // loose B
*((UInt32*)pointer) = singlePixel << 8; // adjust A R G back
}
}
endTime = TimeSpan.FromSeconds(pf.NextValue());
bmp.UnlockBits(bd);
bmp.Dispose();
}
MessageBox.Show((endTime - startTime).TotalMilliseconds.ToString());
}
誰かが、いくつかのビットワイズ操作を行うよりも、ポインターを増やすことがより費用のかかる操作である理由を明確にすることができますか?
.NET 4フレームワークを使用しています。
このようなことはC ++に当てはまるでしょうか?
NB。 32ビットvs 64ビット2つの方法の比率は等しいが、両方の方法は64対32ビットで20%遅いほど?
編集:PorgesとArulによって示唆されているように、これはメモリの読み取り数が減少し、頭上で分岐する可能性があります。
編集2:
いくつかのテストの後、メモリから読む時間が短いことが答えだと思われます。
このコードでは、画像幅が5で割り切れると仮定すると、400%が高速になります。
[StructLayout(LayoutKind.Sequential,Pack = 1)]
struct PixelContainer {
public UInt32 pixel1;
public UInt32 pixel2;
public UInt32 pixel3;
public UInt32 pixel4;
public UInt32 pixel5;
}
次に、これを使用します:
int readSize = sizeof(PixelContainer);
// .....
for (var pointer = line; pointer < line + bd.Stride; pointer += readSize)
{
multiPixel = *((PixelContainer*)pointer);
multiPixel.pixel1 &= 0xFFFFFF00u;
multiPixel.pixel2 &= 0xFFFFFF00u;
multiPixel.pixel3 &= 0xFFFFFF00u;
multiPixel.pixel4 &= 0xFFFFFF00u;
multiPixel.pixel5 &= 0xFFFFFF00u;
*((PixelContainer*)pointer) = multiPixel;
}
解決
これは、として知られる手法です ループの展開。 主なパフォーマンスの利点は、分岐オーバーヘッドを減らすことから来るはずです。
サイドノートとして、ビットマスクを使用して、少しスピードアップできます。
*((UInt64 *)pointer) &= 0xFFFFFF00FFFFFF00ul;
他のヒント
ポインターが遅くなるのではなく、メモリから読むのはポインターを増やすことではありません。 32ビットユニットを使用すると、2倍の読み取りを行っています。
64ビットバージョンで2回ではなく1回書くと、もう一度より速く見つける必要があります。