VB.NET, C#, C++/CLI による処理速度の比較
VisionProでプログラムを組む場合、.NETでつかえる開発言語から選ぶことになります。
一般的なところでは、Visual Basic, C#, C++/CLIあたりだと思います。
迷信?
といったような印象を持っている人もいると思います。
実際私も試してみるまで、やっぱC++だよね。と思っていました。
で、数年前ですが試してみました。追試をするのは面倒なので過去の実績です。
簡単なフィルタの処理速度を比較しました。フィルタの内容は特に意味があるわけではなくて、単純に画像内の全部の画素を操作するプログラムを組むために適当(いい加減な)フィルタを作ってみただけです。
ソースコードは言語間の違いを修正する以外はなるべく同じになるようにしました。
評価環境(ちょっと古すぎますね。。。)
- Windows XP SP2
- Xeon 2.8GHz
- 2GB RAM
- Visual Studio 2005 Professional SP3
- VisionPro 4.4RTM
- CVL6.5CR2 Prerelease fot Testing
- 200万画素の画像
作ったもの、ビルド条件など
すべてコンソールアプリケーションでGUIはありません。
速度計測時は、「デバッグなしで開始(Ctrl+F5)」にて実行し、JIT最適化も機能した状態
ビルドは、「Release」。コンパイルオプションは明記無い場合はデフォルト。
どの言語も「Release」の場合、最適化がONになっている。
CVLと書いてあるものは、比較のためにCVL(C++)で作ったものです。
でどうなった?
言語 | 速度(ms) | 条件 | メモ |
---|---|---|---|
VB.NET | 20000 | ポインタが存在しないため、CogImage8Grey::GetPixel()/SetPixel()を使用した | |
400 | ポインタが存在しないため、マネージ配列へ一括コピー後に処理を行い、処理結果もマネージ配列へ格納。変換後画像へ一括コピー。 | ||
270 | 「整数オーバーフローのチェックを解除」をON | ↑ | |
C# | 20000 | ポインタを使わずに、CogImage8Grey::GetPixel()/SetPixel()を使用した | |
280 | ポインタを使わずに、マネージ配列へ一括コピー後に処理を行い、処理結果もマネージ配列へ格納。変換後画像へ一括コピー。 | ||
230 | unsafe宣言し、ポインタを使用した。画像データは、アンマネージヒープを直接操作した。 | ||
C++/CLI | 190 | マネージコードのみ。画像データは、アンマネージヒープを直接操作した。 | |
150 | フィルター処理部をアンマネージコード。画像データは、アンマネージヒープを操作した。 | ||
CVL | 150 |
ちょっと表が見難いですが、どう思いますか?
VB, C#が意外と検討しています。特にunsafeを使わない限り、VBとC#は同格です。
で、C++/CLIはさすがです。WindowsのネイティブコードであるCVLの結果と遜色ありません。
考察?
VB, C#は生産性が高いのですが、意図しない「暗黙の・・・」が結構有りそれが速度に影響を与える場合があります。
明示的な記述が要求されるC++/CLI の方が、とっつきにくいですが慣れればよさそうです。
でも作りやすさ、学習しやすさはVB,C#が圧倒的に上です。今回のようにフィルタを作るように画素を直接操作する場合以外はVB,C#で十分だと思います。
C#は、最適化が甘いようです。コードの記述方法による速度変化が大きくソースコードレベルの最適化作業が必要になります。
VBよりも工夫できる余地が大きい分なおさらです。ただし、この結果はVisual Studio 2005ですから、最新の2010では異なった結果になるかも知れません。
やっぱり追試が必要ですね。。。
C++/CLI は、最適化が優れており、下手にソースコードで工夫をするとかえって遅くなることが多いです。素直なコードが一番速いためメンテナンス性に優れます。
でも、構文はC++以上に面倒であり、特にManage codeとUnmanage codeが混在するMixモードで作る場合は、結構気をつける必要があります。
まぁ、MixモードがC++/CLIの最大の魅力でもあります。
結論
おまけ
この記事へのアクセスが非常に多いようなので参考までに実験に使ったソースコードの一部を添付しておきます。VisionProがなければ動きませんし、VisualStudio2010では一部構文が変わったためビルドできない場所もありました。あくまでもご参考までに。
ちなみに、フィルタの処理に意味はありません。なんとなくそれらしくメモリにアクセスするためのフィルタです。
C#のマネージ配列にコピーするコード
/***************** * C#による画素操作サンプル * * safeコードによる画素操作をするため、 * CogImage8Grey::GetPixel()/SetPixel()を使用した場合 約20秒 * Manage のByte[] にいったん画像をコピーした場合 280ms * * XEON 2.8GHz にて、200万画素の画像 * *****************/ static void Main(string[] args) { int _numThreshold = 40; int _numIgnThresh = 10; #region 画像取り込み CogImageFileCDB版 // 画像を取り込む CogImageFileTool使用 CogImageFileCDB CDB = new CogImageFileCDB(); CDB.Open("c:/temp/rsi/rsitest.idb", CogImageFileModeConstants.Read); if (CDB.Count < 1) { // Console.WriteLine("画像がありません"); return; } Console.WriteLine("IDBオープン"); #endregion // フィルター処理 for (int nImg = 0; nImg < CDB.Count; nImg++) { CogImage8Grey srcImage = (CogImage8Grey)CDB[nImg]; // _OutputImage を用意する int w = srcImage.Width; int h = srcImage.Height; CogImage8Grey dstImage = new CogImage8Grey(); dstImage.Allocate(w, h); // できれば真っ黒に塗りつぶしたい // 時間計測開始 Stopwatch sw = new Stopwatch(); sw.Start(); ICogImage8PixelMemory mem1 = srcImage.Get8GreyPixelMemory(CogImageDataModeConstants.Read, 0, 0, w, h); ICogImage8PixelMemory mem2 = dstImage.Get8GreyPixelMemory(CogImageDataModeConstants.Write, 0, 0, w, h); // 次の行までのオフセット int srcStride = mem1.Stride; int dstStride = mem2.Stride; // Managed の Array 作成 Byte[] srcArray = new Byte[ srcStride * h ]; Byte[] dstArray = new Byte[ dstStride * h ]; // Imageの先頭ポインタを取得 IntPtr pSrc = (IntPtr)mem1.Scan0; IntPtr pDst = (IntPtr)mem2.Scan0; // 内容をコピー Marshal.Copy(pSrc, srcArray, 0, srcArray.Length); Marshal.Copy(pDst, dstArray, 0, dstArray.Length); // 3x3カーネルのループ Byte[] wk = new Byte[9]; for (int y = 1; y < h - 1; y++) { int yoff0 = (y - 1) * srcStride; // 前の行 int yoff1 = y * srcStride; // この行 int yoff2 = (y + 1) * srcStride; // 次の行 for (int x = 1; x < w - 1; x++) { // 012 3x3を配列に // 345 // 678 wk[0] = srcArray[yoff0 + x - 1]; wk[1] = srcArray[yoff0 + x]; wk[2] = srcArray[yoff0 + x + 1]; wk[3] = srcArray[yoff1 + x - 1]; wk[4] = srcArray[yoff1 + x]; wk[5] = srcArray[yoff1 + x + 1]; wk[6] = srcArray[yoff2 + x - 1]; wk[7] = srcArray[yoff2 + x]; wk[8] = srcArray[yoff2 + x + 1]; int sum = 0, max = 0, min = 999; for (int i = 0; i < 9; i++) { sum += wk[i]; if (max < wk[i]) max = wk[i]; if (min > wk[i]) min = wk[i]; } int ave = sum / 9; int range = max - min; int tmp; if (range > _numIgnThresh && _numThreshold > range) { if (wk[4] > ave) tmp = Math.Min(wk[4] + range, 255); else tmp = Math.Max(wk[4] - range, 0); } else { tmp = wk[4]; } dstArray[yoff1 + x] = (byte)tmp; } } // Managed の配列から、Imageへ書き戻し Marshal.Copy(dstArray, 0, pDst, dstArray.Length); // メモリブロックの取り扱い終了 mem1.Dispose(); mem2.Dispose(); sw.Stop(); double dTime = (double)sw.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0; Console.WriteLine("処理時間[{0}] : {1} ms", nImg, dTime.ToString("####.00")); } // 後始末 if (CDB != null) CDB.Close(); }
C#のunsafeを使ってポインタ操作するコード
/***************** * C#による画素操作サンプル * * メモリに対するポインター操作を行うために unsafe 宣言している 230ms * unsafe によるデメリット * ・JITによる最適化が行われない * * XEON 2.8GHz にて、200万画素の画像 * *****************/ unsafe static void Main(string[] args) { int _numThreshold = 40; int _numIgnThresh = 10 ; #region 画像取り込み CogImageFileCDB版 // 画像を取り込む CogImageFileTool使用 CogImageFileCDB CDB = new CogImageFileCDB(); CDB.Open("c:/temp/rsi/rsitest.idb", CogImageFileModeConstants.Read); if (CDB.Count < 1) { // Console.WriteLine("画像がありません"); return; } Console.WriteLine("IDBオープン"); #endregion // フィルター処理 for (int nImg = 0; nImg < CDB.Count; nImg++) { CogImage8Grey srcImage = (CogImage8Grey)CDB[nImg]; // _OutputImage を用意する int w = srcImage.Width; int h = srcImage.Height; CogImage8Grey dstImage = new CogImage8Grey(); dstImage.Allocate(w, h); // できれば真っ黒に塗りつぶしたい // 時間計測開始 Stopwatch sw = new Stopwatch(); sw.Start(); // イメージ先頭のポインタを取得 ICogImage8PixelMemory mem1 = srcImage.Get8GreyPixelMemory(CogImageDataModeConstants.Read, 0, 0, w, h); ICogImage8PixelMemory mem2 = dstImage.Get8GreyPixelMemory(CogImageDataModeConstants.Write, 0, 0, w, h); // 次の行へのoffset int s1 = mem1.Stride; int s2 = mem2.Stride; // 先頭のポインタを取得 // C# でポインタを利用するためには、unsafe キーワードが必要。 // またプロジェクトのビルドプロパティーでもunsafeを許可する必要がある // fiexd により、メモリを固定する必要があると思うが、宣言するとすでに固定されているとエラーになる // ICogImage8PixelMemory::Scan0 にてすでに固定されているのかも。 Byte* pSrc = (Byte*)(mem1.Scan0 + 1 + 1 * s1); // (1,1)から開始 Byte* pDst = (Byte*)(mem2.Scan0 + 1 + 1 * s2); // 3x3カーネルのループ Byte[] wk = new Byte[9]; for (int y = 1; y < h - 1; y++) { for (int x = 1; x < w - 1; x++) { // 012 3x3を配列に // 345 // 678 wk[0] = *((Byte*)(pSrc + x-1 - s1)); wk[1] = *((Byte*)(pSrc + x - s1)); wk[2] = *((Byte*)(pSrc + x+1 - s1)); wk[3] = *((Byte*)(pSrc + x-1)); wk[4] = *((Byte*)(pSrc + x )); wk[5] = *((Byte*)(pSrc + x+1)); wk[6] = *((Byte*)(pSrc + x-1 + s1)); wk[7] = *((Byte*)(pSrc + x + s1)); wk[8] = *((Byte*)(pSrc + x+1 + s1)); int sum = 0, max = 0, min = 999; for (int i = 0; i < 9; i++) { sum += wk[i]; if (max < wk[i]) max = wk[i]; if (min > wk[i]) min = wk[i]; } int ave = sum / 9; int range = max - min; int tmp; if (range > _numIgnThresh && _numThreshold > range) { if (wk[4] > ave) tmp = Math.Min(wk[4] + range, 255); else tmp = Math.Max(wk[4] - range, 0); } else { tmp = wk[4]; } *((Byte*)(pDst + x)) = (Byte)tmp; } pSrc += s1; pDst += s2; } // メモリブロックの取り扱い終了 mem1.Dispose(); mem2.Dispose(); sw.Stop(); double dTime = (double)sw.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0; Console.WriteLine("処理時間[{0}] : {1} ms", nImg, dTime.ToString("####.00")); } // 後始末 if (CDB != null) CDB.Close(); }
#VB.NETのマネージ配列を利用するコード
'***************** '* C#による画素操作サンプル '* '* 画素操作をするため、 '* CogImage8Grey::GetPixel()/SetPixel()を使用した場合 約20秒 '* Manage のByte[] にいったん画像をコピーした場合 400ms ' 上に加えて「整数オーバーフローのチェックを解除」した場合 270ms '* '* XEON 2.8GHz にて、200万画素の画像 '* '*****************/ Sub Main() Dim _numThreshold As Integer = 40 Dim _numIgnThresh As Integer = 10 ' 画像を取り込む CogImageFileTool使用 Dim CDB As CogImageFileCDB = New CogImageFileCDB() CDB.Open("c:/temp/rsi/rsitest.idb", CogImageFileModeConstants.Read) If (CDB.Count < 1) Then Console.WriteLine("画像がありません") Return End If ' フィルター処理 Dim nImg As Integer = 0 For nImg = 0 To CDB.Count - 1 Step 1 Dim srcImage As CogImage8Grey = CDB.Item(nImg) ' _OutputImage を用意する Dim w As Integer = srcImage.Width Dim h As Integer = srcImage.Height Dim dstImage As New CogImage8Grey() dstImage.Allocate(w, h) ' できれば真っ黒に塗りつぶしたい ' 時間計測開始 Dim sw As New Stopwatch sw.Start() ' なぜか、完全名で記述しないとICogImage8PixelMemoryがPrivate扱いでエラーになる Dim mem1 As Cognex.VisionPro.ICogImage8PixelMemory = srcImage.Get8GreyPixelMemory(CogImageDataModeConstants.Read, 0, 0, w, h) Dim mem2 As Cognex.VisionPro.ICogImage8PixelMemory = dstImage.Get8GreyPixelMemory(CogImageDataModeConstants.Write, 0, 0, w, h) ' 次の行までのオフセット Dim srcStride As Integer = mem1.Stride Dim dstStride As Integer = mem2.Stride ' Managed の Array 作成 Dim srcArray(srcStride * h) As Byte Dim dstArray(dstStride * h) As Byte ' Imageの先頭ポインタを取得 Dim pSrc As IntPtr = mem1.Scan0 Dim pDst As IntPtr = mem2.Scan0 ' 内容をコピー Marshal.Copy(pSrc, srcArray, 0, srcArray.Length) Marshal.Copy(pDst, dstArray, 0, dstArray.Length) ' 3x3カーネルのループ Dim wk(8) As Byte Dim y As Integer Dim x As Integer For y = 1 To h - 2 Step 1 Dim yoff0 As Integer = (y - 1) * srcStride ' 前の行 Dim yoff1 As Integer = y * srcStride ' この行 Dim yoff2 As Integer = (y + 1) * srcStride ' 次の行 For x = 1 To w - 2 Step 1 ' 012 3x3を配列に ' 345 ' 678 wk(0) = srcArray(yoff0 + x - 1) wk(1) = srcArray(yoff0 + x) wk(2) = srcArray(yoff0 + x + 1) wk(3) = srcArray(yoff1 + x - 1) wk(4) = srcArray(yoff1 + x) wk(5) = srcArray(yoff1 + x + 1) wk(6) = srcArray(yoff2 + x - 1) wk(7) = srcArray(yoff2 + x) wk(8) = srcArray(yoff2 + x + 1) Dim sum As Integer = 0 Dim max As Integer = 0 Dim min As Integer = 999 Dim i As Integer = 0 For i = 0 To 8 sum = sum + wk(i) If (max < wk(i)) Then max = wk(i) If (min > wk(i)) Then min = wk(i) Next i Dim ave As Integer = sum / 9 Dim range As Integer = max - min Dim tmp As Integer If (range > _numIgnThresh And _numThreshold > range) Then If (wk(4) > ave) Then tmp = Math.Min(wk(4) + range, 255) Else tmp = Math.Max(wk(4) - range, 0) End If Else tmp = wk(4) End If dstArray(yoff1 + x) = tmp Next x Next y ' Managed の配列から、Imageへ書き戻し Marshal.Copy(dstArray, 0, pDst, dstArray.Length) ' メモリブロックの取り扱い終了 mem1.Dispose() mem2.Dispose() sw.Stop() Dim dTime As Double = sw.ElapsedTicks / Stopwatch.Frequency * 1000.0 Console.WriteLine("処理時間[{0}] : {1} ms", nImg, dTime.ToString("####.00")) Next nImg ' 後始末 If CDB Is Nothing Then CDB.Close() End Sub
C++/CLIのマネージコード
/*** managed .NET環境でコードが動作する 言語仕様は C++/CLI になる C#と違って、unsafe等のペナルティーなしにポインターが使えている ***/ #pragma managed void managed_main(Byte *pSrcUL, Byte *pDstUL, int w, int h, int srcStride, int dstStride, int _numThreshold, int _numIgnThresh ) { // 先頭のポインタを取得 Byte* pSrc = pSrcUL + 1 + srcStride; // (1,1)から開始 Byte* pDst = pDstUL + 1 + dstStride; // 3x3カーネルのループ // この配列宣言が下のループ内にある場合、JIT最適化によりエラーになる。 // 原因不明。JIT最適化なし(VSから実行)ならば動作する。 array<Byte>^ wk = gcnew array<Byte>(9); for (int y = 1; y < h - 1; y++) { for (int x = 1; x < w - 1; x++) { // 012 3x3を配列に // 345 // 678 wk[0] = *((Byte*)(pSrc + x-1 - srcStride)); wk[1] = *((Byte*)(pSrc + x - srcStride)); wk[2] = *((Byte*)(pSrc + x+1 - srcStride)); wk[3] = *((Byte*)(pSrc + x-1)); wk[4] = *((Byte*)(pSrc + x )); wk[5] = *((Byte*)(pSrc + x+1)); wk[6] = *((Byte*)(pSrc + x-1 + srcStride)); wk[7] = *((Byte*)(pSrc + x + srcStride)); wk[8] = *((Byte*)(pSrc + x+1 + srcStride)); int sum = 0, max = 0, min = 999; for (int i = 0; i < 9; i++) { sum += wk[i]; if (max < wk[i]) max = wk[i]; if (min > wk[i]) min = wk[i]; } int ave = sum / 9; int range = max - min; int tmp; if (range > _numIgnThresh && _numThreshold > range) { if (wk[4] > ave) tmp = (wk[4]+range < 255 ? wk[4]+range : 255); else tmp = (wk[4]-range > 0 ? wk[4]-range : 0); } else { tmp = wk[4]; } *((Byte*)(pDst + x)) = (Byte)tmp; } pSrc += srcStride; pDst += dstStride; } }
C++/CLIのアンマネージコード
/*** unmanaged ネイティブ環境でコードが動作する 言語仕様は C++ と同じになる CVL等も普通に使えるはず ***/ #pragma unmanaged #define BYTE unsigned char void unmanaged_main(Byte *pSrcUL, Byte *pDstUL, int w, int h, int srcStride, int dstStride, int _numThreshold, int _numIgnThresh ) { // 先頭のポインタを取得 BYTE *pSrc = pSrcUL + 1 + srcStride; // (1,1)から開始 BYTE *pDst = pDstUL + 1 + dstStride; // 3x3カーネルのループ BYTE wk[9];// = gcnew array<Byte>(9); for (int y = 1; y < h - 1; y++) { for (int x = 1; x < w - 1; x++) { // 012 3x3を配列に // 345 // 678 wk[0] = *((BYTE*)(pSrc + x-1 - srcStride)); wk[1] = *((BYTE*)(pSrc + x - srcStride)); wk[2] = *((BYTE*)(pSrc + x+1 - srcStride)); wk[3] = *((BYTE*)(pSrc + x-1)); wk[4] = *((BYTE*)(pSrc + x )); wk[5] = *((BYTE*)(pSrc + x+1)); wk[6] = *((BYTE*)(pSrc + x-1 + srcStride)); wk[7] = *((BYTE*)(pSrc + x + srcStride)); wk[8] = *((BYTE*)(pSrc + x+1 + srcStride)); int sum = 0, max = 0, min = 999; for (int i = 0; i < 9; i++) { sum += wk[i]; if (max < wk[i]) max = wk[i]; if (min > wk[i]) min = wk[i]; } int ave = sum / 9; int range = max - min; int tmp; if (range > _numIgnThresh && _numThreshold > range) { if (wk[4] > ave) tmp = (wk[4]+range < 255 ? wk[4]+range : 255); else tmp = (wk[4]-range > 0 ? wk[4]-range : 0); } else { tmp = wk[4]; } *((BYTE*)(pDst + x)) = (BYTE)tmp; } pSrc += srcStride; pDst += dstStride; } }