使用INTEL TBB进行可扩展的内存分配

时间:2017-10-02 09:59:47

标签: c++ memory-management tbb scalable

我想在RAM上分配大约40 GB。我的第一次尝试是:

#include <iostream>
#include <ctime>

int main(int argc, char** argv)
{
    unsigned long long  ARRAYSIZE = 20ULL * 1024ULL * 1024ULL * 1024ULL;
    unsigned __int16 *myBuff = new unsigned __int16[ARRAYSIZE];  // 3GB/s  40GB / 13.7 s
    unsigned long long i = 0;
    const clock_t begintime = clock(); 
    for (i = 0; i < ARRAYSIZE; ++i){
    myBuff[i] = 0;
    }
    std::cout << "finish:  " << float(clock() - begintime) / CLOCKS_PER_SEC << std::endl;
    std::cin.get();
    delete [] myBuff;
    return 0;
}

内存写入速度约为3 GB / s,这对我的高性能系统来说并不令人满意。

所以我尝试了以下英特尔Cilk Plus:

    /*
    nworkers =  5;  8.5 s ==> 4.7 GB/s
    nworkers =  8;  8.2 s ==> 4.8 GB/s
    nworkers =  10; 9   s ==> 4.5 GB/s
    nworkers =  32; 15  s ==> 2.6 GB/s
    */

#include "cilk\cilk.h"
#include "cilk\cilk_api.h"
#include <iostream>
#include <ctime>

int main(int argc, char** argv)
{
    unsigned long long  ARRAYSIZE = 20ULL * 1024ULL * 1024ULL * 1024ULL;
    unsigned __int16 *myBuff = new unsigned __int16[ARRAYSIZE];
    if (0 != __cilkrts_set_param("nworkers", "32")){
    std::cout << "Error" << std::endl;
    }
    const clock_t begintime = clock();
    cilk_for(long long j = 0; j < ARRAYSIZE; ++j){
    myBuff[j] = 0;
    }
    std::cout << "finish:  " << float(clock() - begintime) / CLOCKS_PER_SEC << std::endl;
    std::cin.get();
    delete [] myBuff;
    return 0;
}

结果在代码上方评论。可以看出,nworkers = 8的速度加快了。 但是规模越大的nworkers,分配越慢。我想也许是因为线程锁定。 所以我尝试了英特尔TBB提供的可扩展分配器:

#include "tbb\task_scheduler_init.h"
#include "tbb\blocked_range.h"
#include "tbb\parallel_for.h"
#include "tbb\scalable_allocator.h"
#include "cilk\cilk.h"
#include "cilk\cilk_api.h"
#include <iostream>
#include <ctime>
// No retry loop because we assume that scalable_malloc does
// all it takes to allocate the memory, so calling it repeatedly
// will not improve the situation at all
//
// No use of std::new_handler because it cannot be done in portable
// and thread-safe way (see sidebar)
//
// We throw std::bad_alloc() when scalable_malloc returns NULL
//(we return NULL if it is a no-throw implementation)

void* operator new (size_t size) throw (std::bad_alloc)
{
    if (size == 0) size = 1;
    if (void* ptr = scalable_malloc(size))
        return ptr;
    throw std::bad_alloc();
}

void* operator new[](size_t size) throw (std::bad_alloc)
{
    return operator new (size);
}

void* operator new (size_t size, const std::nothrow_t&) throw ()
{
    if (size == 0) size = 1;
    if (void* ptr = scalable_malloc(size))
        return ptr;
    return NULL;
}

void* operator new[](size_t size, const std::nothrow_t&) throw ()
{
    return operator new (size, std::nothrow);
}

void operator delete (void* ptr) throw ()
{
    if (ptr != 0) scalable_free(ptr);
}

void operator delete[](void* ptr) throw ()
{
    operator delete (ptr);
}

void operator delete (void* ptr, const std::nothrow_t&) throw ()
{
    if (ptr != 0) scalable_free(ptr);
}

void operator delete[](void* ptr, const std::nothrow_t&) throw ()
{
    operator delete (ptr, std::nothrow);
}



int main(int argc, char** argv)
{
    unsigned long long  ARRAYSIZE = 20ULL * 1024ULL * 1024ULL * 1024ULL;
    tbb::task_scheduler_init tbb_init;
    unsigned __int16 *myBuff = new unsigned __int16[ARRAYSIZE];
    if (0 != __cilkrts_set_param("nworkers", "10")){
        std::cout << "Error" << std::endl;
    }
    const clock_t begintime = clock();
    cilk_for(long long j = 0; j < ARRAYSIZE; ++j){
        myBuff[j] = 0;
        }
    std::cout << "finish:  " << float(clock() - begintime) / CLOCKS_PER_SEC << std::endl;

    std::cin.get();
    delete [] myBuff;
    return 0;
}

(以上代码改编自James Reinders的英特尔TBB书,O'REILLY) 但结果几乎与之前的尝试相同。我设置TBB_VERSION环境变量,看看我是否真的使用 Scalable_malloc和获取的信息在这张图片中(nworkers = 32):

https://www.dropbox.com/s/y1vril3f19mkf66/TBB_Info.png?dl=0

我愿意知道我的代码出了什么问题。我希望内存写入速度至少约为40 GB / s 我应该如何正确使用可扩展分配器?
有人可以提供一个使用INTEL TBB的可扩展分配器的简单验证示例吗?

环境: Intel Xeon CPU E5-2690 0 @ 2.90 GHz(2个处理器),224 GB RAM(2 * 7 * 16 GB)DDR3 1600 MHz,Windows Server 2008 R2 Datacenter, Microsoft Visual Studio 2013和英特尔C ++编译器2017。

3 个答案:

答案 0 :(得分:3)

期待什么

来自wikipedia:“DDR3-xxx表示数据传输速率,描述DDR芯片,而PC3-xxxx表示理论带宽(最后两位被截断),用于描述组装的DIMM。带宽为通过每秒传输并乘以8来计算。这是因为DDR3内存模块在64位数据位宽的总线上传输数据,并且由于一个字节包含8位,这相当于每次传输8个字节的数据。“ p>

因此单个模块DDR3-1600最大可达1600 * 8 = 12800 MB / s 拥有系统4个通道(每个处理器),您应该能够达到:

12800 * 4 = 51200 MB / s - 51.2 GB / s,这就是CPU specifications

中的说明

你有两个CPU和8个通道:你应该能够达到它的两倍,并行工作。但是,您的系统是NUMA系统 - 在这种情况下,内存放置很重要......

但是

每个频道可以放置多个内存库。当在通道中放置更多模块时,您正在减少可用时间 - 例如,PC-1600可以表现为PC-1333或更低 - 通常在主板规格中报告。示例here

你有七个模块 - 你的频道没有相等......你的带宽受最慢频道的限制。建议将通道填充相等。

如果你被降频到1333,你可以期待: 每通道1333 * 8 = 10666 MB / s:

每CPU 42 GB / s

然而

通道在寻址空间中交错分布,在清零内存块时使用所有通道。只有在访问带条带访问的内存时,才能遇到性能问题。

内存分配不是内存访问

TBB可扩展分配允许许多线程优化内存分配。也就是说,分配时不存在全局锁定,并且内存分配不会被其他线程活动限制。这就是OS分配器中经常发生的事情。

在您的示例中,您根本没有使用多个分配,只有一个主线程。而您正在尝试获取最大内存带宽。使用不同的分配器时,内存访问不会改变。

阅读评论我看到你想要优化内存访问。

优化内存访问

用memset()调用替换归零循环,让编译器优化/内联它。 - / O2应该足够了。

原理

英特尔编译器用优化的内在函数/内联调用替换了许多库调用(memset,memcpy,...)。在这种情况下 - 即将一大块ram归零 - 内联并不重要,但使用优化的内在函数非常重要:它将使用优化版本的流指令:SSE4.2 / AVX

然而,基本的libc memset将胜过任何手写循环。至少在Linux上。

答案 1 :(得分:1)

我至少可以告诉你为什么你不能超过25

根据英特尔,您的CPU最大RAM带宽为51.2GB / s 根据维基百科

,DDR3-1600的最大带宽为25.6GB / s

这意味着必须使用至少2个RAM通道才能超过25个。这几乎是不断的,如果你想要40-50。

为此,您必须知道操作系统如何在ram插槽中拆分内存地址,并以一种并行内存访问实际上位于parmlel中可访问的2 ram地址的方式对循环进行并行化。如果并行化访问相同的&#39;接近的时间地址,它们可能在同一个ram stick上,只使用一个ram通道,从而将速率限制在理论上的25GB / s。 您可能甚至需要能够在多个ram插槽中的单独地址中以块的形式拆分分配的内容,具体取决于ram地址在插槽上的并行化方式。

答案 2 :(得分:1)

(继续评论)

这里有一些内置函数性能测试供参考。它测量保留(通过调用VirtualAlloc)并将物理RAM(通过调用VirtualLock)40 GB内存块所需的时间。

#include <sdkddkver.h>
#include <Windows.h>

#include <intrin.h>

#include <array>
#include <iostream>
#include <memory>
#include <fcntl.h>
#include <io.h>
#include <stdio.h>

void
Handle_Error(const ::LPCWSTR psz_what)
{
    const auto error_code{::GetLastError()};
    ::std::array<::WCHAR, 512> buffer;
    const auto format_result
    (
        ::FormatMessageW
        (
            FORMAT_MESSAGE_FROM_SYSTEM
        ,   nullptr
        ,   error_code
        ,   0
        ,   buffer.data()
        ,   static_cast<::DWORD>(buffer.size())
        ,   nullptr
        )
    );
    const auto formatted{0 != format_result};
    if(!formatted)
    {
        const auto & default_message{L"no description"};
        ::memcpy(buffer.data(), default_message, sizeof(default_message));
    }
    buffer.back() = L'\0'; // just in case
    _setmode(_fileno(stdout), _O_U16TEXT);
    ::std::wcout << psz_what << ", error # " << error_code << ": " << buffer.data() << ::std::endl;
    system("pause");
    exit(-1);
}

void
Enable_Previllege(const ::LPCWSTR psz_name)
{
    ::TOKEN_PRIVILEGES tkp{};
    if(FALSE == ::LookupPrivilegeValueW(nullptr, psz_name, ::std::addressof(tkp.Privileges[0].Luid)))
    {
        Handle_Error(L"LookupPrivilegeValueW call failed");
    }
    const auto this_process_handle(::GetCurrentProcess()); // Returns pseudo handle (HANDLE)-1, no need to call CloseHandle
    ::HANDLE token_handle{};
    if(FALSE == ::OpenProcessToken(this_process_handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ::std::addressof(token_handle)))
    {
        Handle_Error(L"OpenProcessToken call failed");
    }
    if(NULL == token_handle)
    {
        Handle_Error(L"OpenProcessToken call returned invalid token handle");
    }
    tkp.PrivilegeCount = 1;
    tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    if(FALSE == ::AdjustTokenPrivileges(token_handle, FALSE, ::std::addressof(tkp), 0, nullptr, nullptr))
    {
        Handle_Error(L"AdjustTokenPrivileges call failed");
    }
    if(FALSE == ::CloseHandle(token_handle))
    {
        Handle_Error(L"CloseHandle call failed");
    }
}

int main()
{
    constexpr const auto bytes_count{::SIZE_T{40} * ::SIZE_T{1024} * ::SIZE_T{1024} * ::SIZE_T{1024}};
    //  Make sure we can set asjust working set size and lock memory.
    Enable_Previllege(SE_INCREASE_QUOTA_NAME);
    Enable_Previllege(SE_LOCK_MEMORY_NAME);
    //  Make sure our working set is sufficient to hold that block + some little extra.
    constexpr const ::SIZE_T working_set_bytes_count{bytes_count + ::SIZE_T{4 * 1024 * 1024}};
    if(FALSE == ::SetProcessWorkingSetSize(::GetCurrentProcess(), working_set_bytes_count, working_set_bytes_count))
    {
        Handle_Error(L"SetProcessWorkingSetSize call failed");
    }
    //  Start timer.
    ::LARGE_INTEGER start_time;
    if(FALSE == ::QueryPerformanceCounter(::std::addressof(start_time)))
    {
        Handle_Error(L"QueryPerformanceCounter call failed");
    }
    //  Run test.
    const ::SIZE_T min_large_page_bytes_count{::GetLargePageMinimum()}; // if 0 then not supported
    const ::DWORD allocation_flags
    {
        (0u != min_large_page_bytes_count)
        ?
        ::DWORD{MEM_COMMIT | MEM_RESERVE} // | MEM_LARGE_PAGES} // need to enable large pages support for current user first
        :
        ::DWORD{MEM_COMMIT | MEM_RESERVE}
    };
    if((0u != min_large_page_bytes_count) && (0u != (bytes_count % min_large_page_bytes_count)))
    {
        Handle_Error(L"bytes_cout value is not suitable for large pages");
    }
    constexpr const ::DWORD protection_flags{PAGE_READWRITE};
    const auto p{::VirtualAlloc(nullptr, bytes_count, allocation_flags, protection_flags)};
    if(!p)
    {
        Handle_Error(L"VirtualAlloc call failed");
    }
    if(FALSE == ::VirtualLock(p, bytes_count))
    {
        Handle_Error(L"VirtualLock call failed");
    }
    //  Stop timer.
    ::LARGE_INTEGER finish_time;
    if(FALSE == ::QueryPerformanceCounter(::std::addressof(finish_time)))
    {
        Handle_Error(L"QueryPerformanceCounter call failed");
    }
    //  Cleanup.
    if(FALSE == ::VirtualUnlock(p, bytes_count))
    {
        Handle_Error(L"VirtualUnlock call failed");
    }
    if(FALSE == ::VirtualFree(p, 0, MEM_RELEASE))
    {
        Handle_Error(L"VirtualFree call failed");
    }
    //  Report results.
    ::LARGE_INTEGER freq;
    if(FALSE == ::QueryPerformanceFrequency(::std::addressof(freq)))
    {
        Handle_Error(L"QueryPerformanceFrequency call failed");
    }
    const auto elapsed_time_ms{((finish_time.QuadPart - start_time.QuadPart) * ::LONGLONG{1000u}) / freq.QuadPart};
    const auto rate_mbytesps{(bytes_count * ::SIZE_T{1000}) / static_cast<::SIZE_T>(elapsed_time_ms)};
    _setmode(_fileno(stdout), _O_U16TEXT);
    ::std::wcout << elapsed_time_ms << " ms " << rate_mbytesps << " MB/s " << ::std::endl;
    system("pause");
    return 0;
}

在我的系统上,Windows 10 Pro,Xeon E3 1245 V5 @ 3.5GHz,64 GB DDR4(4x16),输出:

  

8188 ms 5245441250 MB / s

这段代码似乎只使用了一个核心。 CPU specs的最大值为34.1 GB / s。你的第一个代码片段需要大约11.5秒(在释放模式下VS不会省略循环)。

启用大页面可能会稍微改善一下。另请注意,VirtualLock页面无法进行交换,与手动将其归零的方案不同。大页面根本无法进行交换。