VB.NET, C#, C++/CLI による処理速度の比較

VisionProでプログラムを組む場合、.NETでつかえる開発言語から選ぶことになります。
一般的なところでは、Visual Basic, C#, C++/CLIあたりだと思います。

迷信?

  • .NETは遅い
  • VBC#のような簡易な言語は遅い
  • VBはめちゃくちゃ遅い

といったような印象を持っている人もいると思います。
実際私も試してみるまで、やっぱ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を使わない限り、VBC#は同格です。
で、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の最大の魅力でもあります。

結論

  • VB,C#に明確な差はなく実用十分な性能を持っている。どちらにするかは趣味で選んでいい
  • C++/CLIはそれでも頭ひとつ優れている。でも書くのは面倒くさい
  • 道具は適材適所で。普通はVB,C#でOK。ここぞというところだけ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;
    }
}