我想在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。
答案 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
页面无法进行交换,与手动将其归零的方案不同。大页面根本无法进行交换。