Visual C++ 2005 への移行と生成コードの速度低下

「Visual C++ 6.0/ .NET 2003 から Visual C++ 2005 に移行したら生成されたプログラムのパフォーマンスが悪化した」という話はまあありがちといえばありがちかもしれません.少なくともあって不思議ではない話です.
一般にこの手の議論が面白いのはプロファイリングを行ってどこがどのように regression したかある程度分かってからです.単にこのコードが速くなった/遅くなっただけという雑談は読むだけ時間の無駄なのですが,得てしてネット上で目につくのはそういった雑談の方が多いように思います.系をブラックボックスのまま扱って結果だけ見て一喜一憂というのは私の本業の方でも陥りやすい罠だったりするので,こうやって他人事のように言っている場合ではなかったりしますけど.
まあいずれにせよ自分で問題を突き止められる人は解決まで自分で行ってしまうため,問題の存在自体があまり表に出てこないのかもしれません.というような話を思い出したのが以下のインシデントです.

Bug Details: RTM VS2005 Optimiser broken
http://lab.msdn.microsoft.com/productfeedback/viewfeedback.aspx?feedbackid=b53f7db8-d955-4b66-9ada-28e33a3ae22a

また,Microsoft の方からの回答で示されている以下の MSDN Forums のスレッドも参考になります.

VC2005 %50 Slower then VC2003
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=165078&SiteID=1&PageID=2

ちなみに MSDN Forums のこのスレッドでのオチは _mbscmp 内のロケールに関する処理が Visual C++ 2005 で変更されたために performance regression が起きていたというものでした.議論の流れは以下のようになります.

  1. グラフィックスアプリケーションということで最初の投稿の段階では Pentium 4 最適化や SSE 最適化について疑われていた.
  2. 次にプロファイルを取ってみるとどうも STL の map と (標準 C++ のではない) string クラスの組み合わせが遅そうという話に.
  3. CString を利用したテストコードでの議論.

途中にサンプルコードが出てきたので,手元の環境でちょっとだけ実験してみました.少しだけコードに手を入れたので,実験に用いたコードを以下に示します.

/*
for Visual C++ .NET 2003
/O2 /G7 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /FD /EHsc /MT /GS /arch:SSE2 /W3 /c /Zi /TP
/O2 /G7 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /FD /EHsc /MT /GS /arch:SSE2 /W3 /c /Zi /TP

for Visual C++ 2005
/O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /FD /EHsc /MD /arch:SSE2 /W3 /c /Zi /TP
/O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /FD /EHsc /MD /arch:SSE2 /W3 /c /Zi /TP */

#include <windows.h>
#include <tchar.h>
#include <map>

using namespace std;

//#define _ATL_CSTRING_NO_CRT
#include <atlstr.h>
using namespace ATL;

class Object
{
public:
    CString m_csObject;

    Object()
    {
    }

    Object( const Object& rhs )
    {
        m_csObject = rhs.m_csObject;
    }

    Object( CString::PCXSTR pcszName )
    {
        m_csObject = pcszName;
    }

#ifdef CSTRING_COPY_ON_WRITE
    Object( const CString& name )
    {
        m_csObject = name;
    }
#endif

    virtual ~Object()
    {
    }
};

typedef map<CString,Object> Object_Map;

// Global Map
Object_Map g_Map;

// Test Function
CString::PCXSTR getObjectName( CString::PCXSTR pcszObject )
{
    Object_Map::iterator itr = g_Map.find( pcszObject );
    if(g_Map.end() != itr)
    {
        return (itr->second).m_csObject;
    }
    return NULL;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int                i;
    int                j;
    CString            cs;
    double             el;
    LARGE_INTEGER      s1, s2, f;
    ::QueryPerformanceFrequency(&f);

    ::QueryPerformanceCounter(&s1);
    for(i=0;i<500000;i++)
    {
        cs.Format( _T("Object:%d"), i );
        g_Map[cs] = Object( cs );
    }
    ::QueryPerformanceCounter(&s2);
    el = (double)(s2.QuadPart - s1.QuadPart);
    el /= double(f.QuadPart);
    printf( "Add Time: %f ms\n", el*1000 );

    // Now search
    ::QueryPerformanceCounter(&s1);
    for(j=0;j<10;j++)
    {
        for(i=0;i<500000;i++)
        {
            cs.Format( _T("::A%d"), i );
            getObjectName( cs );
        }
    }
    ::QueryPerformanceCounter(&s2);
    el = (double)(s2.QuadPart - s1.QuadPart);
    el /= double(f.QuadPart);
    printf( "Search Time: %f ms\n", el*1000 );

    return 0;
}

これを Windows XP Professional x64Edition, AMD Athlon 64 3500+ という環境で実行してみました.上のスレッドでは MBCS (Multi-Byte Character Set) の CString で実験していますが,Unicode 使用時の結果及び CRT を使わない CString 実装 (_ATL_CSTRING_NO_CRT) の結果もあわせて示しています.まずは "Add Time" の結果から.

Add Time Result
Character CString VC++ .NET 2003 [msec] VC++ 2005 [msec]
MBCS CRT 2732 3503
MBCS API 6549 6638
Unicode CRT 2133 1999
Unicode API 1703 1574

確かに MBCS + CRT では Visual C++ 2005 への移行で performance regression があるようです.個人的には MBCS + API の組み合わせと Unicode + API の組み合わせが対照的なのが興味深いです.
次に "Search Time" の結果.

Search Time Result
Character CString VC++ .NET 2003 [msec] VC++ 2005 [msec]
MBCS CRT 8579 12596
MBCS API 31898 32239
Unicode CRT 9719 9590
Unicode API 5755 6075

こちらも "Add Time" と同じ傾向が見て取れます.
最後におまけですが,CString のコピーコンストラクタが呼び出されるようにしたバージョンの "Add Time" の結果です.

Add Time Result (CString Copy-On-Write)
Character CString VC++ .NET 2003 [msec] VC++ 2005 [msec]
MBCS CRT 2548 3313
MBCS API 6386 6443
Unicode CRT 1988 1750
Unicode API 1550 1377

元々のソースコードは Object クラスのコンストラクタで文字列ポインタ経由で CString をコピーしていますが,CString は内部に参照カウンタつきのバッファを持っていて,単純なコピーはバッファの参照カウンタを増やすだけで済ませることができます.このため一旦文字列ポインタを経由させるのではなく,直接 CString 同士のコピーコンストラクタを呼び出すように出来た方が若干パフォーマンスが改善するかもね,という動機で測定してみました.いずれも若干のパフォーマンス向上が見られるようです.
暇があったら VTune での比較結果も載せてみたいと思います.