Unsafe, But Fast!
6 |
想當年在初學C#時,知道C#有unsafe這種東西,可以解開.NET對指標(Pointer)的封印,允許像C語言一樣透過指標直接存取記憶體。對C語言沒有深厚基礎的我,模糊地知道直接存取記憶體效能較佳,卻不很是清楚它的應用時機。最近胡亂玩了一些視覺元素識別的題目,參考一些圖形運算前輩的範例程式,才訝異地發現,原來密集大量運算的場合,就是unsafe橫掃千軍的絕佳舞台。
舉個簡單的例子,假設我們在某本書的綠色書背擷取到一塊包含ISBN條碼的影像,打算透過演算法找出條碼區所在位置。由於綠色書皮與白色條碼區在顏色上有明顯區別,因此可使用比對顏色RGB值的方式,將"不夠白"的地方一律塗黑,以二分方將有色區跟白色區明顯區隔開來。(請不要問我如果書皮是白色的怎麼辦? 這裡只是舉一個方便展示程式的範例咩! 只是範例只是範例只是範例...)
這程式一點也不難寫, 利用Bitmap載入圖檔,然後借重GetPixel讀取圖片中每一點的顏色,判斷後決定改為純白或全黑,再以SetPixel寫回去,大功告成!
private void ImgProcTest1()
{
Bitmap bmp = Bitmap.FromFile(
"c:\\temp\\100617\\isbn.bmp") as Bitmap;
int limit = 0x40;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int x = 0; x < bmp.Width; x++)
for (int y = 0; y < bmp.Height; y++)
{
Color c = bmp.GetPixel(x, y);
if (c.R < limit ||
c.B < limit ||
c.G < limit)
bmp.SetPixel(x, y, Color.Black);
else
bmp.SetPixel(x, y, Color.White);
}
sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString());
pictureBox1.Image = bmp;
}
用上面這段程式解析480*320的圖檔,在我的E6400 Dual Core機器上耗時約550ms,約0.5秒搞定,速度還可接受。
不過,直覺上存取每一像素都要呼叫函數的寫法效率不佳,而Bitmap有個LockBits,支援以byte[]方式處理像素,速度應該會提升一些。參考MSDN範例,我們改用LockBits鎖定記憶體,接著用Marshal.Copy將資料複製到byte[],判斷修改完再以Marshal.Copy將資料存回Bitmap,就可得到與上述程式相同的結果。
private void ImgProcTest2()
{
Bitmap bmp = Bitmap.FromFile(
"c:\\temp\\100617\\isbn.bmp") as Bitmap;
int limit = 0x40;
Stopwatch sw = new Stopwatch();
sw.Start();
BitmapData data =
bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);
IntPtr ptr = data.Scan0;
int len = bmp.Width * bmp.Height * 3;
byte[] buff = new byte[len];
Marshal.Copy(ptr, buff, 0, len);
int idx = 0;
for (int x = 0; x < bmp.Width; x++)
for (int y = 0; y < bmp.Height; y++)
{
byte c = (buff[idx] < limit ||
buff[idx+1] < limit ||
buff[idx+2] < limit) ? (byte)0 : (byte)0xff;
buff[idx] = buff[idx + 1] = buff[idx + 2] = c;
idx += 3;
}
Marshal.Copy(buff, 0, ptr, len);
bmp.UnlockBits(data);
sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString());
pictureBox1.Image = bmp;
}
Wow, 測試結果,改用byte[]後只需約40ms,速度增快了14倍!!! 夠快了嗎? 如果今天我們處理的影像來自攝影裝置,那麼40ms代表一秒鐘最多可運算25次,若演算法再複雜一點,這種速度仍不夠理想,只是,還能再快嗎??
嘿... 該unsafe上場了! LockBits後,我們改透過byte*指標直接操作Bitmap記憶體,冒著讀寫不慎程式就會當掉的危險,我們能把速度提升到何等境界呢?
private void ImgProcTest3()
{
Bitmap bmp = Bitmap.FromFile(
"c:\\temp\\100617\\isbn.bmp") as Bitmap;
int limit = 0x40;
Stopwatch sw = new Stopwatch();
sw.Start();
BitmapData data =
bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);
unsafe
{
byte* ptr = (byte*)(data.Scan0);
int padding = data.Stride - (bmp.Width * 3);
for (int i = 0; i < data.Height; i++)
{
for (int j = 0; j < data.Width; j++)
{
//ptr[0] -> B, ptr[1] -> G, ptr[2] -> R
byte c = (ptr[0] < limit ||
ptr[1] < limit ||
ptr[2] < limit) ? (byte)0 : (byte)0xff;
ptr[0] = ptr[1] = ptr[2] = c;
ptr += 3;
}
ptr += padding;
}
}
bmp.UnlockBits(data);
sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString());
pictureBox1.Image = bmp;
}
2ms!! 好說好說,比用.NET byte[]快了20倍而已。這就是為什麼儘管unsafe很不安全,卻仍有其不可取代性的原因吧~~~ 有了unsafe,.NET也有機會寫出效能逼近C的程式碼囉! 但一定要當心,unsafe顧名思義本是一種不安全的寫法,少了.NET的保護,讀寫錯位置很有可能造成嚴重當機,算是簽了生死狀去飆速度,在撰寫程式計算記憶體位址時要格外小心。
Comments
# by TeYoU
「筆記」LockBits 可以搭配這文章一起看 http://www.bobpowell.net/lockingbits.htm
# by Jeffrey
to TeYoU, 謝謝補充,好一篇鞭辟入裡的佳文!
# by 山姆先生
黑大, 看到這一篇我突然想到一個問題, 我一直想要寫一隻在Excel環境下, 能夠將插入(或複製貼上)的圖片反黑的Scripts, 不知道利用VBS有沒有可能達成呢? 還是有其它的方法? 至於為什麼要反黑, 其實是因為我們經常會複製一些類似Dos的黑底白字畫面, 相當浪費印表機的炭粉, 很不環保~ 所以現在都是利用Copy去小畫家自己反黑後再重新貼回Excel, 但還是覺得太浪費時間了... 以上 謝謝
# by cc
可惜。。。Silverlight中不能用unsafe
# by Felix
請問範例中這個 method 的參數有寫錯嗎? Marshal.Copy(buff, 0, ptr, len); prototype 如下: public static void Copy(IntPtr source, byte[] destination, int startIndex, int length)
# by Jeffrey
to Felix, Copy 有多個 Overloading,範例是用這個 public static void Copy (byte[] source, int startIndex, IntPtr destination, int length); https://learn.microsoft.com/zh-tw/dotnet/api/system.runtime.interopservices.marshal.copy?view=net-7.0#system-runtime-interopservices-marshal-copy(system-byte()-system-int32-system-intptr-system-int32)