Mr.C64
Mr.C64

Reputation: 42984

Is new[] faster than Win32's VirtualAlloc?

I was testing the performance of some string pool allocators: I considered the one presented here that calls Virtual­Alloc and then carves out sub-allocations, and a similar implementation using standard C++ (without directly calling any Win32 API) and new[].

I expected the Virtual­Alloc version to be faster, since I thought there should be less overhead than C++ new[]; but the results I observed are the opposite: using new[] seems to result in faster code than using the lower-level Virtual­Alloc.

I ran the test several times (the code is compiled with VS2010 SP1), and the output is something like this:

String pool using VirtualAlloc: 1280.07 ms
String pool using new[]: 799.193 ms

Why is this? Why does new[] seem to be faster than VirtualAlloc?

Test source code follows:

////////////////////////////////////////////////////////////////////////////
// Testing VirtualAlloc vs. new[].
////////////////////////////////////////////////////////////////////////////


#include <string.h>
#include <wchar.h>
#include <algorithm>
#include <exception>
#include <iostream>
#include <new>
#include <ostream>
#include <stdexcept>
#include <string>
#include <vector>
#include <windows.h>
using namespace std;


//--------------------------------------------------------------------------
// String pool allocator using VirtualAlloc, based on this:
// http://blogs.msdn.com/oldnewthing/archive/2005/05/19/420038.aspx
//--------------------------------------------------------------------------
class StringPoolUsingVirtualAlloc
{
public:

    StringPoolUsingVirtualAlloc()
        : m_pchNext(nullptr), 
          m_pchLimit(nullptr), 
          m_phdrCur(nullptr)
    {
        SYSTEM_INFO si;
        GetSystemInfo(&si);
        m_dwGranularity = static_cast<DWORD>( 
            RoundUp( sizeof(HEADER) + MIN_CBCHUNK, si.dwAllocationGranularity 
            ));
    }

    ~StringPoolUsingVirtualAlloc()
    {
        HEADER* phdr = m_phdrCur;
        while (phdr) 
        {
            HEADER * phdrPrev = phdr->m_phdrPrev;
            VirtualFree(phdr, 0, MEM_RELEASE);
            phdr = phdrPrev;
        }
    }

    wchar_t* DuplicateString(const wstring& source)
    {
        return AllocString(source.c_str(), source.c_str() + source.length());
    }

private:
    union HEADER 
    {
        struct 
        {
            HEADER* m_phdrPrev;
            SIZE_T  m_cb;
        };
        wchar_t alignment;
    };

    enum 
    { 
        MIN_CBCHUNK = 32000,
        MAX_CHARALLOC = 1024*1024
    };

    wchar_t*  m_pchNext;
    wchar_t*  m_pchLimit;
    HEADER*   m_phdrCur;
    DWORD     m_dwGranularity;

    static SIZE_T RoundUp(SIZE_T cb, SIZE_T units)
    {
        return ((cb + units - 1) / units) * units;
    }

    wchar_t* AllocString(const wchar_t* pchBegin, const wchar_t* pchEnd)
    {
        SIZE_T cchTotal = pchEnd - pchBegin + 1;
        if (cchTotal > MAX_CHARALLOC) 
            throw length_error("String too big.");

        wchar_t* psz = m_pchNext;
        if (m_pchNext + cchTotal <= m_pchLimit) 
        {
            m_pchNext += cchTotal;
            lstrcpynW(psz, pchBegin, static_cast<int>(cchTotal));
            return psz;
        }

        SIZE_T cbAlloc = RoundUp(cchTotal * sizeof(wchar_t) + sizeof(HEADER), m_dwGranularity);
        BYTE* pbNext = reinterpret_cast<BYTE*>(
            VirtualAlloc(nullptr, cbAlloc, MEM_COMMIT, PAGE_READWRITE));
        if (pbNext == nullptr) 
            throw bad_alloc();

        m_pchLimit = reinterpret_cast<wchar_t*>(pbNext + cbAlloc);
        HEADER* phdrCur = reinterpret_cast<HEADER*>(pbNext);
        phdrCur->m_phdrPrev = m_phdrCur;
        phdrCur->m_cb = cbAlloc;
        m_phdrCur = phdrCur;
        m_pchNext = reinterpret_cast<wchar_t*>(phdrCur + 1);
        return AllocString(pchBegin, pchEnd);
    }

    StringPoolUsingVirtualAlloc(const StringPoolUsingVirtualAlloc &);
    StringPoolUsingVirtualAlloc & operator=(const StringPoolUsingVirtualAlloc &);
};


//--------------------------------------------------------------------------
// String pool allocator that uses standard C++ (no Win32 stuff) and new[].
//--------------------------------------------------------------------------
class StringPoolUsingNew
{
public:

    StringPoolUsingNew()
        : m_pchNext(NULL), 
          m_pchLimit(NULL), 
          m_currChunk(NULL)
    {
    }

    ~StringPoolUsingNew()
    {
        for (auto it = m_chunks.begin(); it != m_chunks.end(); ++it)
            delete *it;
    }

    wchar_t* DuplicateString(const wstring& source)
    {
        return AllocString(source.c_str(), source.c_str() + source.length());
    }

private:

    class Chunk
    {
    public:
        explicit Chunk(size_t maxCharCount)
        {
            m_data = new wchar_t[maxCharCount];
            m_maxCharCount = maxCharCount;
        }

        ~Chunk()
        {
            delete [] m_data;
        }

        wchar_t* Begin()             { return m_data; }
        const wchar_t* Begin() const { return m_data; }
        size_t Length() const        { return m_maxCharCount; }

    private:
        Chunk(const Chunk&);
        Chunk& operator=(const Chunk&);

        wchar_t * m_data;
        size_t m_maxCharCount;
    };

    static const size_t kMinChunkCharCount = 16000;
    static const size_t kMaxCharAlloc = 1024*1024;

    wchar_t*  m_pchNext;
    wchar_t*  m_pchLimit;
    Chunk*    m_currChunk;
    vector<Chunk*> m_chunks;

    wchar_t* AllocString(const wchar_t* pchBegin, const wchar_t* pchEnd)
    {
        const size_t cchTotal = pchEnd - pchBegin + 1;
        if (cchTotal > kMaxCharAlloc) 
            throw length_error("String too big.");

        wchar_t* dest = m_pchNext;
        if (m_pchNext + cchTotal <= m_pchLimit) 
        {
            m_pchNext += cchTotal;
            const size_t copyCount = cchTotal - 1;
            if (copyCount != 0)
                wmemcpy(dest, pchBegin, copyCount);
            dest[copyCount] = L'\0';
            return dest;
        }

        const size_t newChunkSize = max(cchTotal, kMinChunkCharCount);
        Chunk* newChunk = new Chunk(newChunkSize);
        m_chunks.push_back(newChunk);

        m_pchNext = newChunk->Begin();
        m_pchLimit = newChunk->Begin() + newChunk->Length();
        m_currChunk = newChunk;

        return AllocString(pchBegin, pchEnd);
    }

    StringPoolUsingNew(const StringPoolUsingNew&);
    StringPoolUsingNew& operator=(const StringPoolUsingNew&);
};


//------------------------------------------------------------------------
//                          Perf Measurement
//------------------------------------------------------------------------

long long Counter() 
{
    LARGE_INTEGER li;
    QueryPerformanceCounter(&li);
    return li.QuadPart;
}

long long Frequency() 
{
    LARGE_INTEGER li;
    QueryPerformanceFrequency(&li);
    return li.QuadPart;
}

void PrintTime(long long start, long long finish, const char * s) 
{
    cout << s << ": " << (finish - start) * 1000.0 / Frequency() << " ms" << endl;
}


//--------------------------------------------------------------------------
// Test
//--------------------------------------------------------------------------
int main()
{
    static const int kExitOk = 0;
    static const int kExitError = 1;
    try
    {
        long long start = 0;
        long long finish = 0;

        const auto shuffled = []() -> vector<wstring> 
        {
            const wstring lorem[] = {
                L"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",
                L"Maecenas porttitor congue massa. Fusce posuere, magna sed",
                L"pulvinar ultricies, purus lectus malesuada libero,",
                L"sit amet commodo magna eros quis urna.",
                L"Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.",
                L"Pellentesque habitant morbi tristique senectus et netus et",
                L"malesuada fames ac turpis egestas. Proin pharetra nonummy pede.",
                L"Mauris et orci."
            };

            vector<wstring> v;
            for (long long i = 0; i < 400*1000; ++i) 
            {
                for (auto it = begin(lorem); it != end(lorem); ++it) 
                {
                    v.push_back((*it) + L" (#" + to_wstring(i) + L")");
                }
            }
            random_shuffle(v.begin(), v.end());

            return v;
        }();

        start = Counter();
        {
            StringPoolUsingVirtualAlloc pool;
            vector<const wchar_t*> v;
            for (auto it = shuffled.begin(); it != shuffled.end(); ++it)
            {
                v.push_back( pool.DuplicateString(*it) );
            }
        }
        finish = Counter();
        PrintTime(start, finish, "String pool using VirtualAlloc");

        start = Counter();
        {
            StringPoolUsingNew pool;
            vector<const wchar_t*> v;
            for (auto it = shuffled.begin(); it != shuffled.end(); ++it)
            {
                v.push_back( pool.DuplicateString(*it) );
            }
        }
        finish = Counter();
        PrintTime(start, finish, "String pool using new[]");

        return kExitOk;
    }
    catch (const exception& e)
    {
        cerr << "*** ERROR: " << e.what() << endl;
        return kExitError;
    }
}

////////////////////////////////////////////////////////////////////////////

Upvotes: 7

Views: 4568

Answers (3)

Mr.C64
Mr.C64

Reputation: 42984

So, @JamesMcNellis found the main problem, i.e. the fact that lstrcpynW was used in the VirtualAlloc-based pool allocator, instead wmemcpy was used in the new[]-based pool allocator.

I modified the original code, uniformly using wmemcpy, and running the tests several times and calculating the average execution time for each test (excluding the first run).

I also added an HeapAlloc-based pool allocator and a simple vector<wstring> to the benchmarks.

Now the results are:

--- Tests summary ---
VirtualAlloc : 781.671 ms
HeapAlloc    : 806.597 ms
new[]        : 889.792 ms
STL strings  : 1491.36 ms

So, VirtualAlloc seems to be the fastest one (as expected).

Compilable code follows (built with VS2010 SP1 / VC10):

////////////////////////////////////////////////////////////////////////////
// Testing VirtualAlloc vs. HeapAlloc vs. new[] vs. STL strings.
////////////////////////////////////////////////////////////////////////////


#include <string.h>
#include <wchar.h>
#include <algorithm>
#include <exception>
#include <iostream>
#include <new>
#include <ostream>
#include <stdexcept>
#include <string>
#include <vector>
#include <windows.h>
using namespace std;


//--------------------------------------------------------------------------
// String pool allocator using VirtualAlloc, based on this:
// http://blogs.msdn.com/oldnewthing/archive/2005/05/19/420038.aspx
//--------------------------------------------------------------------------
class StringPoolUsingVirtualAlloc
{
public:

    StringPoolUsingVirtualAlloc()
        : m_pchNext(nullptr), 
        m_pchLimit(nullptr), 
        m_phdrCur(nullptr)
    {
        SYSTEM_INFO si;
        GetSystemInfo(&si);
        m_dwGranularity = static_cast<DWORD>( 
            RoundUp( sizeof(HEADER) + MIN_CBCHUNK, si.dwAllocationGranularity 
            ));
    }

    ~StringPoolUsingVirtualAlloc()
    {
        HEADER* phdr = m_phdrCur;
        while (phdr) 
        {
            HEADER * phdrPrev = phdr->m_phdrPrev;
            VirtualFree(phdr, 0, MEM_RELEASE);
            phdr = phdrPrev;
        }
    }

    const wchar_t* DuplicateString(const wstring& source)
    {
        return AllocString(source.c_str(), source.c_str() + source.length());
    }

private:
    union HEADER 
    {
        struct 
        {
            HEADER* m_phdrPrev;
            SIZE_T  m_cb;
        };
        wchar_t alignment;
    };

    enum 
    { 
        MIN_CBCHUNK = 32000,
        MAX_CHARALLOC = 1024*1024
    };

    wchar_t*  m_pchNext;
    wchar_t*  m_pchLimit;
    HEADER*   m_phdrCur;
    DWORD     m_dwGranularity;

    static SIZE_T RoundUp(SIZE_T cb, SIZE_T units)
    {
        return ((cb + units - 1) / units) * units;
    }

    wchar_t* AllocString(const wchar_t* pchBegin, const wchar_t* pchEnd)
    {
        SIZE_T cchTotal = pchEnd - pchBegin + 1;
        if (cchTotal > MAX_CHARALLOC) 
            throw length_error("String too big.");

        wchar_t* psz = m_pchNext;
        if (m_pchNext + cchTotal <= m_pchLimit) 
        {
            m_pchNext += cchTotal;
            wmemcpy(psz, pchBegin, cchTotal);
            return psz;
        }

        SIZE_T cbAlloc = RoundUp(cchTotal * sizeof(wchar_t) + sizeof(HEADER), m_dwGranularity);
        BYTE* pbNext = reinterpret_cast<BYTE*>(
            VirtualAlloc(nullptr, cbAlloc, MEM_COMMIT, PAGE_READWRITE));
        if (pbNext == nullptr) 
            throw bad_alloc();

        m_pchLimit = reinterpret_cast<wchar_t*>(pbNext + cbAlloc);
        HEADER* phdrCur = reinterpret_cast<HEADER*>(pbNext);
        phdrCur->m_phdrPrev = m_phdrCur;
        phdrCur->m_cb = cbAlloc;
        m_phdrCur = phdrCur;
        m_pchNext = reinterpret_cast<wchar_t*>(phdrCur + 1);
        return AllocString(pchBegin, pchEnd);
    }

    StringPoolUsingVirtualAlloc(const StringPoolUsingVirtualAlloc &);
    StringPoolUsingVirtualAlloc & operator=(const StringPoolUsingVirtualAlloc &);
};


//--------------------------------------------------------------------------
// String pool allocator using HeapAlloc, 
// based on the VirtualAlloc allocator.
//--------------------------------------------------------------------------
class StringPoolUsingHeapAlloc
{
public:

    StringPoolUsingHeapAlloc()
        : m_pchNext(nullptr), 
        m_pchLimit(nullptr), 
        m_phdrCur(nullptr)
    {
        m_heap = HeapCreate(0, 0, 0);
        if (m_heap == nullptr)
            throw runtime_error("Can't create an heap with HeapCreate().");

        SYSTEM_INFO si;
        GetSystemInfo(&si);
        m_dwGranularity = static_cast<DWORD>( 
            RoundUp( sizeof(HEADER) + MIN_CBCHUNK, si.dwAllocationGranularity 
            ));
    }

    ~StringPoolUsingHeapAlloc()
    {
        HEADER* phdr = m_phdrCur;
        while (phdr) 
        {
            HEADER * phdrPrev = phdr->m_phdrPrev;
            HeapFree(m_heap, 0, phdr);
            phdr = phdrPrev;
        }
        HeapDestroy(m_heap);
    }

    const wchar_t* DuplicateString(const wstring& source)
    {
        return AllocString(source.c_str(), source.c_str() + source.length());
    }

private:
    union HEADER 
    {
        struct 
        {
            HEADER* m_phdrPrev;
            SIZE_T  m_cb;
        };
        wchar_t alignment;
    };

    enum 
    { 
        MIN_CBCHUNK = 32000,
        MAX_CHARALLOC = 1024*1024
    };

    HANDLE    m_heap;
    wchar_t*  m_pchNext;
    wchar_t*  m_pchLimit;
    HEADER*   m_phdrCur;
    DWORD     m_dwGranularity;

    static SIZE_T RoundUp(SIZE_T cb, SIZE_T units)
    {
        return ((cb + units - 1) / units) * units;
    }

    wchar_t* AllocString(const wchar_t* pchBegin, const wchar_t* pchEnd)
    {
        SIZE_T cchTotal = pchEnd - pchBegin + 1;
        if (cchTotal > MAX_CHARALLOC) 
            throw length_error("String too big.");

        wchar_t* psz = m_pchNext;
        if (m_pchNext + cchTotal <= m_pchLimit) 
        {
            m_pchNext += cchTotal;
            wmemcpy(psz, pchBegin, cchTotal);
            return psz;
        }

        SIZE_T cbAlloc = RoundUp(cchTotal * sizeof(wchar_t) + sizeof(HEADER), m_dwGranularity);
        BYTE* pbNext = static_cast<BYTE*>( HeapAlloc(m_heap, 0, cbAlloc) );
        if (pbNext == nullptr) 
            throw bad_alloc();

        m_pchLimit = reinterpret_cast<wchar_t*>(pbNext + cbAlloc);
        HEADER* phdrCur = reinterpret_cast<HEADER*>(pbNext);
        phdrCur->m_phdrPrev = m_phdrCur;
        phdrCur->m_cb = cbAlloc;
        m_phdrCur = phdrCur;
        m_pchNext = reinterpret_cast<wchar_t*>(phdrCur + 1);
        return AllocString(pchBegin, pchEnd);
    }

    StringPoolUsingHeapAlloc(const StringPoolUsingHeapAlloc &);
    StringPoolUsingHeapAlloc & operator=(const StringPoolUsingHeapAlloc &);
};


//--------------------------------------------------------------------------
// String pool allocator that uses standard C++ (no Win32 stuff) and new[].
//--------------------------------------------------------------------------
class StringPoolUsingNew
{
public:

    StringPoolUsingNew()
        : m_pchNext(NULL), 
        m_pchLimit(NULL), 
        m_currChunk(NULL)
    {
    }

    ~StringPoolUsingNew()
    {
        for (auto it = m_chunks.begin(); it != m_chunks.end(); ++it)
            delete *it;
    }

    const wchar_t* DuplicateString(const wstring& source)
    {
        return AllocString(source.c_str(), source.c_str() + source.length());
    }

private:

    class Chunk
    {
    public:
        explicit Chunk(size_t maxCharCount)
        {
            m_data = new wchar_t[maxCharCount];
            m_maxCharCount = maxCharCount;
        }

        ~Chunk()
        {
            delete [] m_data;
        }

        wchar_t* Begin()             { return m_data; }
        const wchar_t* Begin() const { return m_data; }
        size_t Length() const        { return m_maxCharCount; }

    private:
        Chunk(const Chunk&);
        Chunk& operator=(const Chunk&);

        wchar_t * m_data;
        size_t m_maxCharCount;
    };

    static const size_t kMinChunkCharCount = 16000;
    static const size_t kMaxCharAlloc = 1024*1024;

    wchar_t*  m_pchNext;
    wchar_t*  m_pchLimit;
    Chunk*    m_currChunk;
    vector<Chunk*> m_chunks;

    wchar_t* AllocString(const wchar_t* pchBegin, const wchar_t* pchEnd)
    {
        const size_t cchTotal = pchEnd - pchBegin + 1;
        if (cchTotal > kMaxCharAlloc) 
            throw length_error("String too big.");

        wchar_t* dest = m_pchNext;
        if (m_pchNext + cchTotal <= m_pchLimit) 
        {
            m_pchNext += cchTotal;
            const size_t copyCount = cchTotal - 1;
            if (copyCount != 0)
                wmemcpy(dest, pchBegin, copyCount);
            dest[copyCount] = L'\0';
            return dest;
        }

        const size_t newChunkSize = max(cchTotal, kMinChunkCharCount);
        Chunk* newChunk = new Chunk(newChunkSize);
        m_chunks.push_back(newChunk);

        m_pchNext = newChunk->Begin();
        m_pchLimit = newChunk->Begin() + newChunk->Length();
        m_currChunk = newChunk;

        return AllocString(pchBegin, pchEnd);
    }

    StringPoolUsingNew(const StringPoolUsingNew&);
    StringPoolUsingNew& operator=(const StringPoolUsingNew&);
};


//--------------------------------------------------------------------------
// This is just a simple vector<wstring>, to compare performance of this 
// simple and easy approach vs. the other pool allocators.
//--------------------------------------------------------------------------
class StringPoolVectorOfString
{
public:

    StringPoolVectorOfString()
    {
    }

    ~StringPoolVectorOfString()
    {
    }

    const wchar_t* DuplicateString(const wstring& source)
    {
        m_strings.push_back(source);
        return m_strings.back().c_str();
    }

private:
    // Simplest case: a STL vector of STL strings
    vector<wstring> m_strings;

    StringPoolVectorOfString(const StringPoolVectorOfString&);
    StringPoolVectorOfString& operator=(const StringPoolVectorOfString&);
};


//------------------------------------------------------------------------
//                          Perf Measurement
//------------------------------------------------------------------------

long long Counter() 
{
    LARGE_INTEGER li;
    QueryPerformanceCounter(&li);
    return li.QuadPart;
}

long long Frequency() 
{
    LARGE_INTEGER li;
    QueryPerformanceFrequency(&li);
    return li.QuadPart;
}


//--------------------------------------------------------------------------
// Tests
//--------------------------------------------------------------------------

// Prints the first N strings in a vector-like container.
template <typename Container>
void PrintFirst(const Container & c, const size_t firstN)
{
    const size_t n = min(firstN, c.size());
    for (size_t i = 0; i < n; i++)
        wcout << "#" << (i+1) << ": " << c[i] << '\n';
    wcout << endl;
}

// Prints the first N strings using the specified allocator.
template <typename Allocator>
void VerifyAllocator(const vector<wstring>& source, const size_t firstN, const char* allocatorName)
{
    const size_t n = min(firstN, source.size());

    Allocator alloc;
    vector<const wchar_t*> v;

    for (size_t i = 0; i < n; i++)
    {
        v.push_back( alloc.DuplicateString(source[i]) );
    }

    wcout << allocatorName << " :\n";
    PrintFirst(v, n);
}

// Tests a given allocator, returning the execution time in ms.
template <typename Allocator>
double TestAllocator(const vector<wstring>& source, const char* allocatorName)
{
    wcout << "Testing " << allocatorName << " : ";
    long long start = Counter();
    {
        Allocator alloc;
        vector<const wchar_t*> v;

        for (auto it = source.begin(); it != source.end(); ++it)
        {
            v.push_back( alloc.DuplicateString(*it) );
        }
    }
    long long finish = Counter();
    const double time = (finish - start) * 1000.0 / Frequency(); // ms

    wcout << time << " ms\n";
    return time;
}

// Calculates the average in a vector of doubles.
double Average(const vector<double>& data)
{
    if (data.empty())
        throw invalid_argument("Can't compute average of empty vector.");

    double sum = data[0];
    const size_t count = data.size();
    for (size_t i = 1; i < count; ++i)
    {
        sum += data[i];
    }
    return (sum / count);
}

// App entry-point ("test driver").
int main()
{
    static const int kExitOk = 0;
    static const int kExitError = 1;
    try
    {
        wcout << '\n';
        wcout << "Testing VirtualAlloc vs. HeapAlloc vs. new[] allocators vs STL strings.\n";
        wcout << "-----------------------------------------------------------------------\n\n"; 

        wcout << "Preparing some strings for testing...\n";

        const auto shuffled = []() -> vector<wstring> 
        {
            const wstring lorem[] = {
                L"Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",
                L"Maecenas porttitor congue massa. Fusce posuere, magna sed",
                L"pulvinar ultricies, purus lectus malesuada libero,",
                L"sit amet commodo magna eros quis urna.",
                L"Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.",
                L"Pellentesque habitant morbi tristique senectus et netus et",
                L"malesuada fames ac turpis egestas. Proin pharetra nonummy pede.",
                L"Mauris et orci."
            };

            vector<wstring> v;
#ifdef _DEBUG
            static const int kLoopCount = 10;
#else
            static const int kLoopCount = 400*1000;
#endif
            for (long long i = 0; i < kLoopCount; ++i) 
            {
                for (auto it = begin(lorem); it != end(lorem); ++it) 
                {
                    v.push_back((*it) + L" (#" + to_wstring(i) + L")");
                }
            }
            random_shuffle(v.begin(), v.end());

            return v;
        }();

        wcout << "Total string count: " << shuffled.size() << "\n\n";
        wcout << "Some verification output ...\n\n";
        wcout << "Original array of strings :\n";
        PrintFirst(shuffled, 5);

        VerifyAllocator<StringPoolUsingVirtualAlloc>(shuffled, 5, "VirtualAlloc");
        VerifyAllocator<StringPoolUsingHeapAlloc>(shuffled, 5, "HeapAlloc");
        VerifyAllocator<StringPoolUsingNew>(shuffled, 5, "new[]");
        VerifyAllocator<StringPoolVectorOfString>(shuffled, 5, "vector<wstring>");

        vector<double> timeVirtualAlloc;
        vector<double> timeHeapAlloc;
        vector<double> timeNew;
        vector<double> timeStlString;

        static const int kTestCount = 10;

        // First execution tests are discarded.
        wcout << "\nWarm up... discard first tests execution.\n";
        TestAllocator<StringPoolUsingVirtualAlloc>(shuffled, "VirtualAlloc");
        TestAllocator<StringPoolUsingHeapAlloc>(shuffled, "HeapAlloc");
        TestAllocator<StringPoolUsingNew>(shuffled, "new[]");
        TestAllocator<StringPoolVectorOfString>(shuffled, "vector<wstring>");

        // Run the tests several times and compute the average for each test.
        for (int i = 0; i < kTestCount; i++)
        {
            wcout << "\nTest loop #" << (i+1) << ":\n";
            timeVirtualAlloc.push_back( TestAllocator<StringPoolUsingVirtualAlloc>(shuffled, "VirtualAlloc") );
            timeHeapAlloc.push_back( TestAllocator<StringPoolUsingHeapAlloc>(shuffled, "HeapAlloc") );
            timeNew.push_back( TestAllocator<StringPoolUsingNew>(shuffled, "new[]") );
            timeStlString.push_back( TestAllocator<StringPoolVectorOfString>(shuffled, "vector<wstring>") );
        }

        // Print average times
        wcout << "\n\n--- Tests summary ---\n";
        wcout << "VirtualAlloc : " << Average(timeVirtualAlloc) << " ms\n";
        wcout << "HeapAlloc    : " << Average(timeHeapAlloc) << " ms\n";
        wcout << "new[]        : " << Average(timeNew) << " ms\n";
        wcout << "STL strings  : " << Average(timeStlString) << " ms\n";
        wcout << '\n';

        return kExitOk;
    }
    catch (const exception& e)
    {
        wcerr << "\n*** ERROR: " << e.what() << '\n';
        return kExitError;
    }
}


////////////////////////////////////////////////////////////////////////////

Upvotes: 2

James McNellis
James McNellis

Reputation: 355187

Yes, calling new[] repeatedly is much faster than calling VirtualAlloc repeatedly.

First, it is important to understand what new T[N] does. The new operator allocates storage by calling operator new[]. At least since Visual C++ 2010, operator new[] simply calls malloc, which calls the Windows API HeapAlloc to allocate storage from the CRT heap. Prior to Visual C++ 2012, each CRT has its own heap, created via HeapCreate. In Visual C++ 2012, the CRT uses the process heap, obtained via GetProcessHeap. From a performance standpoint, it doesn't matter which heap is used.

VirtualAlloc is used to map pages of memory into the virtual address space of a process. This function is used when you need control over whole pages. For example, if you want to allocate storage to hold executable code, you need to use VirtualAlloc so that you can change the permissions on that storage to allow execution. VirtualAlloc is not optimized for general purpose memory allocation.

For that, you need a heap, which maps a large region of address space at a time, then services allocation requests from that mapped address space. A heap does not have to map and unmap virtual pages every time an allocation is requested (and, also importantly, a heap does not have to zero memory every time an allocation is performed).

When I run your original benchmark, I get the following result:

String pool using VirtualAlloc: 1162.45 ms
String pool using new[]: 625.842 ms

I replaced your usage of VirtualAlloc with HeapAlloc. To do this, I created a private heap for the allocator using HeapCreate(0, 0, 0), then replaced the calls to VirtualAlloc and VirtualFree with calls to HeapAlloc and HeapFree from this private heap. (Note that I did not use the process heap, because as I explained above, new[] uses that heap, so using that heap also here could change the performance of the new[] allocator.) The results of my modified allocator are as follows:

String pool using HeapAlloc: 919.853 ms
String pool using new[]: 636.515 ms

Well, that's extremely disappointing! We improved the performance of the custom allocator by 21%, but it's still much slower than new[]. What's up with that?

The profiler helpfully pointed out what the problem is: your benchmark is comparing apples and oranges. Your new[]-based allocator uses wmemcpy to copy strings, but your VirtualAlloc-based allocator uses lstrcpyn. wmemcpy simply calls memcpy, which has an intrinsic form, so it can be fully inlined with the insanely fast intrinsic form. lstrcpyn is a Windows API function that cannot be inlined. Your VirtualAlloc-based allocator doesn't stand a chance!

I replaced the use of lstrcpyn with wmemcpy. The results are as follows:

String pool using HeapAlloc: 636.149 ms
String pool using new[]: 655.479 ms

And these are the results that we expect: they perform roughly the same, with new[] being just a little bit slower, probably because of the small overhead of calling through operator new and malloc.

Upvotes: 17

Mats Petersson
Mats Petersson

Reputation: 129454

Because new will make one call to VirtualAlloc (or, more likely, HeapAlloc) for quite a bit of memory at once, then use that for several of your calls to new, where a call VirtualAlloc will do exactly what you asked for, alloocate precisely what you asked for. Equally when releasing memory with delete it is faster than VirtualFree, because larger lots of memory is freed at once.

It's exactly the same as using fgetc is faster than ReadFile - sure, if you read one gigabyte at once, ReadFile is probably a fair bit faster tan calling fgetc a gazillion times, but if you read one byte at a time, ReadFile will be much heaver on the system than using fgetc, which will read several (probably 4KB) of data in one go, and then dole out a character at a time from that buffer until it's empty.

Upvotes: 7

Related Questions