Масштабируемое распределение памяти с использованием INTEL TBB

Я хочу выделить около 40 ГБ оперативной памяти. Моя первая попытка была:

#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 ГБ / с, что было недостаточно для моей высокопроизводительной системы.

Поэтому я попробовал Intel 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;
}

Результаты прокомментированы выше кода. Как видно, скорость для рабочих = 8.
Но чем крупнее nworkers, тем медленнее выделение. Я подумал, может быть, это связано с блокировкой нитями.
Поэтому я попробовал масштабируемый распределитель, предоставляемый Intel 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;
}

(Приведенный выше код взят из книги Intel TBB Джеймса Рейндерса, O’REILLY)
Но результаты почти идентичны предыдущей попытке. Я установил переменную среды TBB_VERSION, чтобы увидеть, действительно ли я использую
Scalable_malloc и полученная информация на этом рисунке (nworkers = 32):

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

Я хочу знать, что не так с моим кодом. Я ожидаю, что скорость записи в память будет не менее 40 ГБ / с.
Как правильно использовать масштабируемый распределитель?
Может ли кто-нибудь представить простой проверенный пример использования масштабируемого распределителя от INTEL TBB?

Среда:
Процессор Intel Xeon E5-2690 0 @ 2,90 ГГц (2 процессора), 224 ГБ ОЗУ (2 * 7 * 16 ГБ) DDR3 1600 МГц, Windows Server 2008 R2 Datacenter,
Microsoft visual studio 2013 и компилятор Intel C ++ 2017.

0

Решение

От википедия: «DDR3-xxx обозначает скорость передачи данных и описывает микросхемы DDR, тогда как PC3-xxxx обозначает теоретическую полосу пропускания (с усеченными двумя последними цифрами) и используется для описания собранных модулей DIMM. Пропускная способность рассчитывается путем принятия передач в секунду и умножения на 8. Это связано с тем, что модули памяти DDR3 передают данные по шине шириной 64 бита, а поскольку байт содержит 8 битов, это соответствует 8 байтам данных на передачу «.

Таким образом, один модуль DDR3-1600 может работать с максимальной скоростью 1600 * 8 = 12800 МБ / с.
Имея в вашей системе 4 канала (на процессор), вы сможете достичь:

12800 * 4 = 51200 МБ / с — 51,2 ГБ / с, как указано в Технические характеристики процессора

А также

Всего у вас два ЦП и 8 каналов: вы должны быть в состоянии достичь двойного из них, работая параллельно. Однако ваша система является системой NUMA — в этом случае имеет значение размещение памяти …

Но

Вы можете поместить более одного банка памяти на канал. При добавлении большего количества модулей в канал вы сокращаете доступное время — например, ПК-1600 может вести себя как ПК-1333 или менее — это обычно указывается в спецификациях материнских плат. пример Вот.

У тебя есть Семь модули — ваши каналы не заполнены одинаково … ваша пропускная способность ограничена самым медленным каналом. Рекомендуется, чтобы каналы были заполнены равными друг другу.

Если вы разогнаны до 1333, вы можете ожидать:
1333 * 8 = 10666 МБ / с на канал:

42 ГБ / с на процессор

тем не мение

Каналы распределены с чередованием в адресном пространстве, вы используете их все при обнулении блоков памяти. Вы можете столкнуться с проблемами производительности только при доступе к памяти с полосатым доступом.

Масштабируемое распределение TBB позволяет МНОГИМ потокам оптимизировать распределение памяти. То есть, при выделении нет глобальной блокировки, и выделение памяти не будет заблокировано ограниченным действием других потоков. Это то, что часто происходит в распределителях ОС.

В вашем примере вы вообще не используете много выделений, только один основной поток. И вы пытаетесь получить максимальную пропускную способность памяти. Доступ к памяти не изменится при использовании разных распределителей.

Читая комментарии я вижу, что вы хотите оптимизировать доступ к памяти.

Замените цикл обнуления одним вызовом memset () и дайте компилятору оптимизировать / встроить его. — / O2 должно быть достаточно для этого.

обоснование

Компилятор Intel заменяет многие библиотечные вызовы (memset, memcpy, …) оптимизированными внутренними / встроенными вызовами. В этом контексте, т. Е. Обнуление большого блока оперативной памяти не имеет значения, но использование оптимизированных встроенных функций очень важно: оно будет использовать оптимизированную версию инструкций потоковой передачи: SSE4.2 / AVX

Однако базовый набор libc превзойдет любой рукописный цикл. По крайней мере на Linux.

3

Другие решения

Я могу хотя бы сказать вам, почему вы не получаете больше 25

Ваш ЦП имеет максимальную пропускную способность ОЗУ 51,2 ГБ / с, согласно Intel
DDR3-1600 имеет максимальную пропускную способность 25,6 ГБ / с согласно википедии

Это означает, что по крайней мере 2 канала ОЗУ должны быть использованы для ожидания более 25. Это почти постоянно, если вы хотите приблизиться к 40-50.

Для этого вам нужно знать, как ОС распределяет адрес памяти по слотам оперативной памяти, и парализовать цикл так, чтобы доступ к памяти параллельно осуществлялся по адресу 2 оперативной памяти, к которому можно получить доступ в parralel. Если распараллеливание обращается по «тем же» временным адресам, которые находятся рядом, они, вероятно, будут находиться на одной и той же оперативной памяти и использовать только один канал оперативной памяти, ограничивая тем самым скорость до теоретических 25 ГБ / с.
Вам может даже понадобиться что-то, что способно разделить распределение по блокам по разным адресам в нескольких слотах оперативной памяти, в зависимости от того, как адреса оперативной памяти распараллелены в слотах.

1

(продолжение из комментариев)

Вот некоторые тесты производительности встроенных функций для справки. Он измеряет время, необходимое для резервирования (по телефону VirtualAlloc) и занести в физическую память (позвонив VirtualLock) 40 ГБ блока памяти.

#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,5 ГГц, 64 ГБ DDR4 (4×16), он выводит:

8188 мс 5245441250 МБ / с

Этот код, кажется, использует только одно ядро. Максимум от Характеристики процессора составляет 34,1 ГБ / с. Ваш первый фрагмент кода занимает ~ 11,5 секунд (в режиме выпуска VS не пропускает цикл).

Включение больших страниц, вероятно, немного улучшит это. Также обратите внимание, что с VirtualLock страницы не могут перейти на своп, в отличие от сценария с обнулением их вручную. Большие страницы не могут идти, чтобы поменяться вообще.

1
По вопросам рекламы [email protected]