読者です 読者をやめる 読者になる 読者になる

C# と C++ : Memory Allocation (3)

.NET

テストコード 1.
マルチスレッド環境で new (delete) を連発することに主眼をおいています.4 スレッド並列*1.4096 要素のポインタ配列を用意して,ループ毎に新たなインスタンスを生成しポイント先を置き換えてゆきます.各インスタンスはランダムに文字数を 1〜16 から選んだ文字列クラスです.疑似乱数列の生成は MersenneTwister 法で統一しました.C++ 版では置き換え時のタイミングで古いインスタンスを delete します.ループ毎にポインタ配列を異なる順序でスキャンするように細工をしてあります.
以下ソースコードです.MersenneTwister による乱数生成ルーチンについてはリンク先から入手してください.C++ 版はちゃんとマルチスレッド版の CRT とリンクしましょうね.お兄さんとの約束です.
C#版.

using System;
using System.Text;
using System.Threading;

class Bench1
{
    static void Main(string args)
    {
        const int numThread = 4;
        
        using( ManualResetEvent beginNotify = new ManualResetEvent(false) )
        {
            WaitHandle waitHandles = new WaitHandle[ numThread ];
            ThreadTest tests = new ThreadTest[numThread];
            
            Random rand = new Random();
            for( int i = 0; i < tests.Length; ++i )
            {
                tests[i] = new ThreadTest( string.Format( "Worker Thread {0}", i ), beginNotify, rand.Next() );
                waitHandles[i] = tests[i].EndNotify;
                tests[i].ThreadStart();
            }
            Thread.Sleep(1000);
            
            DateTime begin = DateTime.Now;
            beginNotify.Set();
            WaitHandle.WaitAll( waitHandles );
            foreach( ThreadTest test in tests )
            {
                test.Dispose();
            }
            DateTime end = DateTime.Now;
            TimeSpan el = end - begin;
            Console.WriteLine( el.ToString() );
        }
    }
}

internal class ThreadTest : IDisposable
{
    private const long LoopNum = 4096;
    private readonly Thread _thread;
    private readonly Random _rand;
    private readonly WaitHandle _beginNotify;
    public readonly ManualResetEvent EndNotify;
    public ThreadTest(string name, WaitHandle beginNotify, int seed)
    {
        this._thread = new Thread( new ThreadStart( this.ThreadMain ) );
        this._thread.Name = name;
        this.EndNotify = new ManualResetEvent(false);
        this._beginNotify = beginNotify;
        this._rand = new jp.takel.PseudoRandom.MersenneTwister( (uint) seed );
    }
    public void ThreadMain()
    {
        this._beginNotify.WaitOne();
        
        string buf = new string[ LoopNum ];
        for( long mul = LoopNum; mul > 0; --mul )
        {
            for( long i = 0; i < LoopNum; ++i )
            {
                buf[ (i * mul) % LoopNum ] = new string( '!', this._rand.Next( 1, 16 ) );
            }
        }
        
        this.EndNotify.Set();
    }
    public void ThreadStart()
    {
        this._thread.Start();
    }
    #region IDisposable メンバ
    
    public void Dispose()
    {
        (this.EndNotify as IDisposable).Dispose();
    }
    
    #endregion
}

C++ 版.

#include <windows.h>
#include <string>
#include "MersenneTwister.h"

struct ThreadTest
{
    static const __int64 LoopNum = 4096;
    HANDLE EndNotify;
    ThreadTest( const std::wstring& name, HANDLE beginNotify, DWORD seed )
        : _rand(seed), _name(name), _beginNotify(beginNotify)
    {
        DWORD id;
        EndNotify = CreateEvent( NULL, TRUE, FALSE, name.c_str() );
        _thread = CreateThread( NULL, 0, ThreadMainStub, static_cast<void*>(this), CREATE_SUSPENDED, &id );
    }
    void ThreadStart()
    {
        ResumeThread( _thread );
    }
    ~ThreadTest()
    {
        WaitForSingleObject( _thread, 10000 );
        CloseHandle( _thread );
        CloseHandle( EndNotify );
    }
private:
    MTRand _rand;
    HANDLE _thread;
    HANDLE _beginNotify;
    std::wstring _name;
    static DWORD WINAPI ThreadMainStub( void* ptr )
    {
        return static_cast<ThreadTest*>(ptr)->ThreadMain();
    }
    DWORD WINAPI ThreadMain()
    {
        WaitForSingleObject( _beginNotify, INFINITE );
        std::wstring* buf[ LoopNum ];
        for( int i = 0; i < LoopNum; ++i )
        {
            buf[i] = NULL;
        }
        for( __int64 mul = LoopNum; mul > 0; --mul )
        {
            for( __int64 i = 0; i < LoopNum; ++i )
            {
                const int index = (i * mul) % LoopNum;
                if( buf[index] != NULL )
                {
                    delete buf[index];
                }
                buf[ index ] = new std::wstring( '!', _rand.randInt( 15 ) + 1 );
            }
        }
        for( int i = 0; i < LoopNum; ++i )
        {
            if( buf[i] != NULL )
            {
                delete buf[i];
            }
        }
        SetEvent( EndNotify );
        ExitThread(0);
    }
};

int main()
{
    const int numThread = 4;

    HANDLE beginNotify = CreateEvent( NULL, TRUE, FALSE, L"begin notify" );
    HANDLE waitHandles[ numThread ];
    {
        ThreadTest* tests[ numThread ];
        srand( GetTickCount() );
        for( int i = 0; i < numThread; ++i )
        {
            wchar_t buf[ 64 ];
            wsprintf( buf, L"Worker Thread %d", i );
            tests[i] = new ThreadTest( buf, beginNotify, rand() );
            waitHandles[i] = tests[i]->EndNotify;
            tests[i]->ThreadStart();
        }
        Sleep(1000);

        const DWORD begin = GetTickCount();
        SetEvent( beginNotify );
        WaitForMultipleObjects( numThread, waitHandles, TRUE, INFINITE );
        const DWORD end = GetTickCount();

        const DWORD min  = (end - begin) / 60000;
        const DWORD sec  = (end - begin - 60000 * min) / 1000;
        const DWORD msec = (end - begin - 60000 * min - 1000 * sec);
        wprintf( L"%d:%d.%d\n", min, sec, msec );

        for( int i = 0; i < numThread; ++i )
        {
            delete tests[i];
        }
    }
    
    return 0;
}

手元の環境*2では無視できない程度の差はついています*3
VTune で見てみると,C++ 版では全 CPU 時間の実に約半分がメモリ処理に消費されてしまっています.具体的にはヒープ管理用リンクリスト操作時の lock 処理が最も多くの時間を消費しており,RtlFreeHeap,RtlAllocateHeap の順に続きます.「あんまりだ」とお嘆きの方は『Visual C++ の概念: 機能の追加 ヒープ競合の回避』あたりをどうぞ.ただこの対策をもってしても,RtlFreeHeap と RtlAllocateHeap が高コストが影響して,大きくパフォーマンスが改善されるということはありませんでした.

*1:これは単にうちの PC 環境が XEON×2 であることに由来します

*2:Intex Xeon(Prestonia) 2.0GHz ×2,DDR DPC2100 ECC 1536MB,Windows XP Professional SP1,.NET Framework 1.1 SP1,Visual C++ .NET 2003, Visual C# 2003

*3:.NET Framework EULA の「ベンチマーク結果公開禁止条項」もあって,「どれぐらい差が付くか」は伏せておきます.この程度のネタでは敢えてリスクは犯したくないということで.条項そのものの有効性についても気になるところではありますけどね.
http://www.itmedia.co.jp/news/0301/23/ne00_benchmark.html
ライセンスが変更されて条件付きでベンチマーク結果は公開可です.