前回の続きになります。C#で作ったプログラムを高速化してみます。
さて、約26秒かかっていたものがどのぐらいの速くなるのか…
Bitmapへのアクセスを高速化
前回のコードで使用していたBitmap.SetPixel()
やBitmap.GetPixel()
は極めて遅い処理です。
なのでここはアンセーフコードを用いて高速なメモリアクセスを実装します。
具体的には、以下の手順になります。
Bitmap.LockBits()
でメモリをロックさせ、BitmapData
を取得します。BitmapData.scan0プロパティ
がピクセルデータ領域の先頭アドレスを持っているため、byte型ポインタでキャストして順次アクセスします。- 最後に
Bitmap.UnlockBits()
でメモリのロックを解除してやります。
※プロジェクトのプロパティでアンセーフコードの実行を許可する必要があります。
これに従って書き換えたコードは以下になります。(リサイズメソッドの部分のみの掲載です)
static Bitmap resizeBicubic(Bitmap bmp, int width, int height) { var a = -1.0d; int sw = bmp.Width, sh = bmp.Height; double wf = (double)sw / width, hf = (double)sh / height; Func<double, byte> trimByte = x => (byte)Math.Min(Math.Max(0, Math.Round(x)), 255); // バイキュービック重み付け関数 Func<double, double> weightFunc = d => d <= 1.0 ? ((a + 2.0) * d * d * d) - ((a + 3.0) * d * d) + 1: d <= 2.0 ? (a * d * d * d) - (5.0 * a * d * d) + (8.0 * a * d) - (4.0 * a) : .0; var dst = new Bitmap(width, height, PixelFormat.Format32bppRgb); // LockBitsしてメモリ固定 var bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb); var dstData = dst.LockBits(new Rectangle(Point.Empty, dst.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb); // プロジェクトのプロパティでアンセーフコードの許可が必要です unsafe { // ピクセルデータのポインタを取得 var bmpScan0 = (byte*)bmpData.Scan0; var dstScan0 = (byte*)dstData.Scan0; for (var iy = 0; iy < height; iy++) { for (var ix = 0; ix < width; ix++) { double wfx = wf * ix, wfy = hf * iy; var x = (int)Math.Truncate(wfx); var y = (int)Math.Truncate(wfy); double r = .0, g = .0, b = .0; for (int jy = y - 1; jy <= y + 2; jy++) { for (int jx = x - 1; jx <= x + 2; jx++) { var w = weightFunc(Math.Abs(wfx - jx)) * weightFunc(Math.Abs(wfy - jy)); if (w == 0) continue; var sx = (jx < 0 || jx > sw - 1) ? x : jx; var sy = (jy < 0 || jy > sh - 1) ? y : jy; // 32bitビットマップなので境界は4byte単位 var sPos = 4 * sx + bmpData.Stride * sy; r += bmpScan0[sPos + 2] * w; g += bmpScan0[sPos + 1] * w; b += bmpScan0[sPos + 0] * w; } } var dPos = 4 * ix + dstData.Stride * iy; dstScan0[dPos + 2] = trimByte(r); dstScan0[dPos + 1] = trimByte(g); dstScan0[dPos + 0] = trimByte(b); } } } bmp.UnlockBits(bmpData); dst.UnlockBits(dstData); return dst; }
C#はポインタなんてあまり使わないから、分からない人には理解できないかもしれませんが、ここで出てくるってことはC#だってポインタ必要な時は意外と多いってことです。
そして、これでどの程度早くなったのか!?
ElapsedTime: 1217.91
前回と比べて20倍以上速くなりました。
(´ヘ`;)ウーム…、でもまだ遅いですね。
ならばもっと速くしてみましょう!
マルチスレッド化
C#の素晴らしいところは簡単にマルチスレッドが使えることです。
今日のPCはマルチコアが当たり前になっていて、シングルスレッドプログラムでは全力で回してるつもりがコアを持て余していることが少なくありません。
ならば大いに余ってるコアを使ってやろうではありませんか。
色々やり方はあるけれども、今回はループの回数が非常に多いのでループをマルチスレッドで回せるようにします。
整数カウントアップのfor文はParalell.For()
でほとんど形を変えずに実装可能です。
ということで即実装。
static Bitmap resizeBicubic(Bitmap bmp, int width, int height) { const double a = -1.0d; int sw = bmp.Width, sh = bmp.Height; double wf = (double)sw / width, hf = (double)sh / height; var dst = new Bitmap(width, height, PixelFormat.Format32bppRgb); var bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb); var dstData = dst.LockBits(new Rectangle(Point.Empty, dst.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb); Func<double, byte> trimByte = x => (byte)Math.Min(Math.Max(0, Math.Round(x)), 255); // バイキュービック重み付け関数 Func<double, double> weightFunc = d => d <= 1.0 ? ((a + 2.0) * d * d * d) - ((a + 3.0) * d * d) + 1: d <= 2.0 ? (a * d * d * d) - (5.0 * a * d * d) + (8.0 * a * d) - (4.0 * a) : .0; unsafe { var bmpScan0 = (byte*)bmpData.Scan0; var dstScan0 = (byte*)dstData.Scan0; //for (var iy = 0; iy < height; iy++) Parallel.For(0, height, iy => { for (var ix = 0; ix < width; ix++) { var wfx = wf * ix; var wfy = hf * iy; var x = (int)wfx; var y = (int)wfy; double r = .0, g = .0, b = .0; for (int jy = y - 1; jy <= y + 2; jy++) { for (int jx = x - 1; jx <= x + 2; jx++) { var w = weightFunc(Math.Abs(wfx - jx)) * weightFunc(Math.Abs(wfy - jy)); if (w == 0) continue; var sx = (jx < 0 || jx >= sw) ? x : jx; var sy = (jy < 0 || jy >= sh) ? y : jy; var sPos = 4 * sx + bmpData.Stride * sy; b += bmpScan0[sPos + 0] * w; g += bmpScan0[sPos + 1] * w; r += bmpScan0[sPos + 2] * w; } } var dPos = 4 * ix + dstData.Stride * iy; dstScan0[dPos + 0] = trimByte(b); dstScan0[dPos + 1] = trimByte(g); dstScan0[dPos + 2] = trimByte(r); } }); } bmp.UnlockBits(bmpData); dst.UnlockBits(dstData); return dst; }
たった2行しか修正していません。
さてさて、効果の程はいくらか!?
ElapsedTime: 397.18
かなり速くなりましたね!(コア数によって結果は異なります)
…でもCPUの性能の割にはまだ遅い!のでさらに速く!
ということで次回はさらなる高速化を行います。
でわでわ。
コメント
レスありがとうございます。
自分でも面積平均法のSSE/AVX化をやっているんですが、なかなか思うようにいきません。(Delphiのインラインアセンブラだとアライメントがムツカシイ…)
C#でもDelphiでも構いません。新しい記事を楽しみにしています。
>lovedelphiさん
はじめまして、コメントありがとうございます。
以前はアセンブラやDelphiに打ち込んでいたのですが、最近は仕事が変わった都合あまり触れる機会がなくなってしまいました。
それと併せて音楽の趣味の方が最近忙しいので休日プログラミングもなかなか手に付かないと言った感じで、執筆が進まず申し訳ないです・・・
面積平均法について、実はC#についてはコードが完成しており、近いうちに記事にしようと思っています。
Delphiを見捨てたわけではないので、カムバックはしたいです。
SSEについてもあきらめてはいませんので、なんとかはやく記事にできるよう努力しますね!(固定小数点のMMXよりは楽だと思いますし)
はじめまして、いつも楽しく拝見しています。
最近はdelphiを使っておられないのですか?
高速化必至なグラフィックス分野ではdelphiはまだまだ現役だと思ってるんですが…
で、個人的には面積平均法とSSEによる高速化の話なんかを期待してます。