Измерение NUMA (неравномерный доступ к памяти). Нет наблюдаемой асимметрии. Зачем?
Я попытался измерить асимметричные эффекты доступа к памяти NUMA и не удалось.
Эксперимент
Выполняется на процессоре Intel Xeon X5570 @2,93 ГГц, 2 процессора, 8 ядер.
В потоке, прикрепленном к ядру 0, я выделяю массив x размером 10 000 000 байт на ядре 0 NUMA node с помощью numa_alloc_local.
Затем я перебираю массив x 50 раз и читаю и записываю каждый байт в массиве. Измерьте прошедшее время, чтобы выполнить 50 итераций.
Затем на каждом из других ядер моего сервера я пишу новый поток и снова измеряю прошедшее время, чтобы выполнить 50 итераций чтения и записи
для каждого байта в массиве x.
Массив x является большим, чтобы минимизировать эффекты кеша. Мы хотим измерить скорость, когда процессор должен полностью работать с ОЗУ для загрузки и хранения, а не при использовании кэшей.
На моем сервере есть два узла NUMA, поэтому я ожидаю, что ядра, имеющие сродство к одному и тому же node, в котором выделяется массив x, чтобы иметь
более быстрая скорость чтения/записи. Я этого не вижу.
Почему?
Возможно, NUMA применима только для систем s > 8-12 ядрами, как я уже сказал, в другом месте?
http://lse.sourceforge.net/numa/faq/
numatest.cpp
#include <numa.h>
#include <iostream>
#include <boost/thread/thread.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <pthread.h>
void pin_to_core(size_t core)
{
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
}
std::ostream& operator<<(std::ostream& os, const bitmask& bm)
{
for(size_t i=0;i<bm.size;++i)
{
os << numa_bitmask_isbitset(&bm, i);
}
return os;
}
void* thread1(void** x, size_t core, size_t N, size_t M)
{
pin_to_core(core);
void* y = numa_alloc_local(N);
boost::posix_time::ptime t1 = boost::posix_time::microsec_clock::universal_time();
char c;
for (size_t i(0);i<M;++i)
for(size_t j(0);j<N;++j)
{
c = ((char*)y)[j];
((char*)y)[j] = c;
}
boost::posix_time::ptime t2 = boost::posix_time::microsec_clock::universal_time();
std::cout << "Elapsed read/write by same thread that allocated on core " << core << ": " << (t2 - t1) << std::endl;
*x = y;
}
void thread2(void* x, size_t core, size_t N, size_t M)
{
pin_to_core(core);
boost::posix_time::ptime t1 = boost::posix_time::microsec_clock::universal_time();
char c;
for (size_t i(0);i<M;++i)
for(size_t j(0);j<N;++j)
{
c = ((char*)x)[j];
((char*)x)[j] = c;
}
boost::posix_time::ptime t2 = boost::posix_time::microsec_clock::universal_time();
std::cout << "Elapsed read/write by thread on core " << core << ": " << (t2 - t1) << std::endl;
}
int main(int argc, const char **argv)
{
int numcpus = numa_num_task_cpus();
std::cout << "numa_available() " << numa_available() << std::endl;
numa_set_localalloc();
bitmask* bm = numa_bitmask_alloc(numcpus);
for (int i=0;i<=numa_max_node();++i)
{
numa_node_to_cpus(i, bm);
std::cout << "numa node " << i << " " << *bm << " " << numa_node_size(i, 0) << std::endl;
}
numa_bitmask_free(bm);
void* x;
size_t N(10000000);
size_t M(50);
boost::thread t1(boost::bind(&thread1, &x, 0, N, M));
t1.join();
for (size_t i(0);i<numcpus;++i)
{
boost::thread t2(boost::bind(&thread2, x, i, N, M));
t2.join();
}
numa_free(x, N);
return 0;
}
Выход
g++ -o numatest -pthread -lboost_thread -lnuma -O0 numatest.cpp
./numatest
numa_available() 0 <-- NUMA is available on this system
numa node 0 10101010 12884901888 <-- cores 0,2,4,6 are on NUMA node 0, which is about 12 Gb
numa node 1 01010101 12874584064 <-- cores 1,3,5,7 are on NUMA node 1, which is slightly smaller than node 0
Elapsed read/write by same thread that allocated on core 0: 00:00:01.767428
Elapsed read/write by thread on core 0: 00:00:01.760554
Elapsed read/write by thread on core 1: 00:00:01.719686
Elapsed read/write by thread on core 2: 00:00:01.708830
Elapsed read/write by thread on core 3: 00:00:01.691560
Elapsed read/write by thread on core 4: 00:00:01.686912
Elapsed read/write by thread on core 5: 00:00:01.691917
Elapsed read/write by thread on core 6: 00:00:01.686509
Elapsed read/write by thread on core 7: 00:00:01.689928
Выполнение 50 итераций чтения и записи по массиву x занимает около 1,7 секунды, независимо от того, какое ядро выполняет чтение и запись.
Обновление:
Размер кэша на моих процессорах составляет 8 Мб, поэтому, возможно, массив 10 МБ Х недостаточно велик, чтобы исключить кеш-эффекты. Я попробовал массив 100Mb x, и
Я попытался выпустить полный забор памяти с помощью __sync_synchronize() внутри моих внутренних петель. Он по-прежнему не обнаруживает никакой асимметрии между узлами NUMA.
Обновление 2:
Я пробовал читать и писать в массив x с помощью __sync_fetch_and_add(). Все еще ничего.
Ответы
Ответ 1
Ах, да! Мистик прав! Каким-то образом предварительная выборка оборудования оптимизирует мои операции чтения/записи.
Если бы это была оптимизация кеша, то форсирование барьера памяти могло бы победить оптимизацию:
c = __sync_fetch_and_add(((char*)x) + j, 1);
но это не имеет никакого значения. Что имеет значение, это умножение моего индекса итератора на премьер-1009, чтобы победить оптимизацию предварительной выборки:
*(((char*)x) + ((j * 1009) % N)) += 1;
С этим изменением ясно выявлена асимметрия NUMA:
numa_available() 0
numa node 0 10101010 12884901888
numa node 1 01010101 12874584064
Elapsed read/write by same thread that allocated on core 0: 00:00:00.961725
Elapsed read/write by thread on core 0: 00:00:00.942300
Elapsed read/write by thread on core 1: 00:00:01.216286
Elapsed read/write by thread on core 2: 00:00:00.909353
Elapsed read/write by thread on core 3: 00:00:01.218935
Elapsed read/write by thread on core 4: 00:00:00.898107
Elapsed read/write by thread on core 5: 00:00:01.211413
Elapsed read/write by thread on core 6: 00:00:00.898021
Elapsed read/write by thread on core 7: 00:00:01.207114
По крайней мере, я думаю, что происходит.
Спасибо, Мистик!
EDIT: ЗАКЛЮЧЕНИЕ ~ 133%
Для тех, кто просто смотрит на это сообщение, чтобы получить общее представление о характеристиках производительности NUMA, вот нижняя строка в соответствии с моими тестами:
Доступ к памяти для нелокального NUMA node имеет примерно 1,33 раза латентность доступа к памяти для локального node.
Ответ 2
Прежде всего хочу отметить, что вы можете дважды проверить, какие ядра находятся на каждом node. Я не помню, что ядра и узлы чередуются именно так.
Кроме того, у вас должно быть 16 потоков из-за HT. (если вы не отключили его)
Другое дело:
У разъемов 1366 Xeon машины только немного NUMA. Поэтому будет трудно увидеть разницу. Эффект NUMA намного более заметен на Opteron 4P.
В таких системах, как ваша, пропускная способность node -to-node на самом деле быстрее, чем пропускная способность процессора к памяти. Поскольку ваш шаблон доступа полностью последователен, вы получаете полную пропускную способность, независимо от того, являются ли данные локальными. Лучше всего измерить латентность. Попробуйте случайный доступ к блоку объемом 1 ГБ вместо потоковой передачи его последовательно.
Последняя вещь:
В зависимости от того, насколько агрессивно оптимизирован ваш компилятор, ваш цикл может быть оптимизирован, поскольку он ничего не делает:
c = ((char*)x)[j];
((char*)x)[j] = c;
Что-то вроде этого гарантирует, что компилятор не будет устранен:
((char*)x)[j] += 1;
Ответ 3
Спасибо за этот тестовый код. Я взял вашу "исправленную" версию и изменил ее на чистый C + OpenMP и добавил несколько тестов на то, как система памяти ведет себя под конфликтом. Вы можете найти новый код здесь.
Ниже приведены некоторые примеры результатов Quad Opteron:
num cpus: 32
numa available: 0
numa node 0 10001000100010000000000000000000 - 15.9904 GiB
numa node 1 00000000000000001000100010001000 - 16 GiB
numa node 2 00010001000100010000000000000000 - 16 GiB
numa node 3 00000000000000000001000100010001 - 16 GiB
numa node 4 00100010001000100000000000000000 - 16 GiB
numa node 5 00000000000000000010001000100010 - 16 GiB
numa node 6 01000100010001000000000000000000 - 16 GiB
numa node 7 00000000000000000100010001000100 - 16 GiB
sequential core 0 -> core 0 : BW 4189.87 MB/s
sequential core 1 -> core 0 : BW 2409.1 MB/s
sequential core 2 -> core 0 : BW 2495.61 MB/s
sequential core 3 -> core 0 : BW 2474.62 MB/s
sequential core 4 -> core 0 : BW 4244.45 MB/s
sequential core 5 -> core 0 : BW 2378.34 MB/s
sequential core 6 -> core 0 : BW 2442.93 MB/s
sequential core 7 -> core 0 : BW 2468.61 MB/s
sequential core 8 -> core 0 : BW 4220.48 MB/s
sequential core 9 -> core 0 : BW 2442.88 MB/s
sequential core 10 -> core 0 : BW 2388.11 MB/s
sequential core 11 -> core 0 : BW 2481.87 MB/s
sequential core 12 -> core 0 : BW 4273.42 MB/s
sequential core 13 -> core 0 : BW 2381.28 MB/s
sequential core 14 -> core 0 : BW 2449.87 MB/s
sequential core 15 -> core 0 : BW 2485.48 MB/s
sequential core 16 -> core 0 : BW 2938.08 MB/s
sequential core 17 -> core 0 : BW 2082.12 MB/s
sequential core 18 -> core 0 : BW 2041.84 MB/s
sequential core 19 -> core 0 : BW 2060.47 MB/s
sequential core 20 -> core 0 : BW 2944.13 MB/s
sequential core 21 -> core 0 : BW 2111.06 MB/s
sequential core 22 -> core 0 : BW 2063.37 MB/s
sequential core 23 -> core 0 : BW 2082.75 MB/s
sequential core 24 -> core 0 : BW 2958.05 MB/s
sequential core 25 -> core 0 : BW 2091.85 MB/s
sequential core 26 -> core 0 : BW 2098.73 MB/s
sequential core 27 -> core 0 : BW 2083.7 MB/s
sequential core 28 -> core 0 : BW 2934.43 MB/s
sequential core 29 -> core 0 : BW 2048.68 MB/s
sequential core 30 -> core 0 : BW 2087.6 MB/s
sequential core 31 -> core 0 : BW 2014.68 MB/s
all-contention core 0 -> core 0 : BW 1081.85 MB/s
all-contention core 1 -> core 0 : BW 299.177 MB/s
all-contention core 2 -> core 0 : BW 298.853 MB/s
all-contention core 3 -> core 0 : BW 263.735 MB/s
all-contention core 4 -> core 0 : BW 1081.93 MB/s
all-contention core 5 -> core 0 : BW 299.177 MB/s
all-contention core 6 -> core 0 : BW 299.63 MB/s
all-contention core 7 -> core 0 : BW 263.795 MB/s
all-contention core 8 -> core 0 : BW 1081.98 MB/s
all-contention core 9 -> core 0 : BW 299.177 MB/s
all-contention core 10 -> core 0 : BW 300.149 MB/s
all-contention core 11 -> core 0 : BW 262.905 MB/s
all-contention core 12 -> core 0 : BW 1081.89 MB/s
all-contention core 13 -> core 0 : BW 299.173 MB/s
all-contention core 14 -> core 0 : BW 299.025 MB/s
all-contention core 15 -> core 0 : BW 263.865 MB/s
all-contention core 16 -> core 0 : BW 432.156 MB/s
all-contention core 17 -> core 0 : BW 233.12 MB/s
all-contention core 18 -> core 0 : BW 232.889 MB/s
all-contention core 19 -> core 0 : BW 202.48 MB/s
all-contention core 20 -> core 0 : BW 434.299 MB/s
all-contention core 21 -> core 0 : BW 233.274 MB/s
all-contention core 22 -> core 0 : BW 233.144 MB/s
all-contention core 23 -> core 0 : BW 202.505 MB/s
all-contention core 24 -> core 0 : BW 434.295 MB/s
all-contention core 25 -> core 0 : BW 233.274 MB/s
all-contention core 26 -> core 0 : BW 233.169 MB/s
all-contention core 27 -> core 0 : BW 202.49 MB/s
all-contention core 28 -> core 0 : BW 434.295 MB/s
all-contention core 29 -> core 0 : BW 233.309 MB/s
all-contention core 30 -> core 0 : BW 233.169 MB/s
all-contention core 31 -> core 0 : BW 202.526 MB/s
two-contention core 0 -> core 0 : BW 3306.11 MB/s
two-contention core 1 -> core 0 : BW 2199.7 MB/s
two-contention core 0 -> core 0 : BW 3286.21 MB/s
two-contention core 2 -> core 0 : BW 2220.73 MB/s
two-contention core 0 -> core 0 : BW 3302.24 MB/s
two-contention core 3 -> core 0 : BW 2182.81 MB/s
two-contention core 0 -> core 0 : BW 3605.88 MB/s
two-contention core 4 -> core 0 : BW 3605.88 MB/s
two-contention core 0 -> core 0 : BW 3297.08 MB/s
two-contention core 5 -> core 0 : BW 2217.82 MB/s
two-contention core 0 -> core 0 : BW 3312.69 MB/s
two-contention core 6 -> core 0 : BW 2227.04 MB/s
two-contention core 0 -> core 0 : BW 3287.93 MB/s
two-contention core 7 -> core 0 : BW 2209.48 MB/s
two-contention core 0 -> core 0 : BW 3660.05 MB/s
two-contention core 8 -> core 0 : BW 3660.05 MB/s
two-contention core 0 -> core 0 : BW 3339.63 MB/s
two-contention core 9 -> core 0 : BW 2223.84 MB/s
two-contention core 0 -> core 0 : BW 3303.77 MB/s
two-contention core 10 -> core 0 : BW 2197.99 MB/s
two-contention core 0 -> core 0 : BW 3323.19 MB/s
two-contention core 11 -> core 0 : BW 2196.08 MB/s
two-contention core 0 -> core 0 : BW 3582.23 MB/s
two-contention core 12 -> core 0 : BW 3582.22 MB/s
two-contention core 0 -> core 0 : BW 3324.9 MB/s
two-contention core 13 -> core 0 : BW 2250.74 MB/s
two-contention core 0 -> core 0 : BW 3305.66 MB/s
two-contention core 14 -> core 0 : BW 2209.5 MB/s
two-contention core 0 -> core 0 : BW 3303.52 MB/s
two-contention core 15 -> core 0 : BW 2182.43 MB/s
two-contention core 0 -> core 0 : BW 3352.74 MB/s
two-contention core 16 -> core 0 : BW 2607.73 MB/s
two-contention core 0 -> core 0 : BW 3092.65 MB/s
two-contention core 17 -> core 0 : BW 1911.98 MB/s
two-contention core 0 -> core 0 : BW 3025.91 MB/s
two-contention core 18 -> core 0 : BW 1918.06 MB/s
two-contention core 0 -> core 0 : BW 3257.56 MB/s
two-contention core 19 -> core 0 : BW 1885.03 MB/s
two-contention core 0 -> core 0 : BW 3339.64 MB/s
two-contention core 20 -> core 0 : BW 2603.06 MB/s
two-contention core 0 -> core 0 : BW 3119.29 MB/s
two-contention core 21 -> core 0 : BW 1918.6 MB/s
two-contention core 0 -> core 0 : BW 3054.14 MB/s
two-contention core 22 -> core 0 : BW 1910.61 MB/s
two-contention core 0 -> core 0 : BW 3214.44 MB/s
two-contention core 23 -> core 0 : BW 1881.69 MB/s
two-contention core 0 -> core 0 : BW 3332.3 MB/s
two-contention core 24 -> core 0 : BW 2611.8 MB/s
two-contention core 0 -> core 0 : BW 3111.94 MB/s
two-contention core 25 -> core 0 : BW 1922.11 MB/s
two-contention core 0 -> core 0 : BW 3049.02 MB/s
two-contention core 26 -> core 0 : BW 1912.85 MB/s
two-contention core 0 -> core 0 : BW 3251.88 MB/s
two-contention core 27 -> core 0 : BW 1881.82 MB/s
two-contention core 0 -> core 0 : BW 3345.6 MB/s
two-contention core 28 -> core 0 : BW 2598.82 MB/s
two-contention core 0 -> core 0 : BW 3109.04 MB/s
two-contention core 29 -> core 0 : BW 1923.81 MB/s
two-contention core 0 -> core 0 : BW 3062.94 MB/s
two-contention core 30 -> core 0 : BW 1921.3 MB/s
two-contention core 0 -> core 0 : BW 3220.8 MB/s
two-contention core 31 -> core 0 : BW 1901.76 MB/s
Если у кого-то будут дальнейшие улучшения, я буду рад услышать о них. Например, это, очевидно, не идеальные измерения полосы пропускания в реальных единицах (вероятно, с помощью, как мы надеемся, постоянного целочисленного фактора).
Ответ 4
несколько комментариев:
- чтобы увидеть структуру NUMA вашей системы (в Linux), вы можете получить графический обзор с помощью утилиты
lstopo
из hwloc библиотека. В частности, вы увидите, какие номера ядер являются членами NUMA node (процессорный сокет)
-
char
, вероятно, не идеальный тип данных для измерения максимальной пропускной способности RAM. Я подозреваю, что с использованием 32-битного или 64-битного типа данных вы можете получить больше данных с таким же количеством циклов процессора.
-
В более общем плане вы также должны проверить, что ваше измерение не ограничено скоростью процессора, а скоростью RAM. Утилита ramspeed, например, разворачивает внутренний цикл в некоторой степени в исходном коде:
for(i = 0; i < blk/sizeof(UTL); i += 32) {
b[i] = a[i]; b[i+1] = a[i+1];
...
b[i+30] = a[i+30]; b[i+31] = a[i+31];
}
EDIT: на поддерживаемых архитектурах ramsmp
на самом деле даже использует "написанный вручную" ассемблерный код для этих циклов
-
L1/L2/L3 Эффекты кеша. Поучительно измерять ширину полосы в GByte/s как функцию размера блока. Вы должны увидеть примерно четыре разные скорости при увеличении размера блока, соответствующего тому, где вы читаете данные (кеши или основная память). Кажется, что у вашего процессора 8 Мбайт кеша Level3 (?), Поэтому ваши 10 миллионов байтов могут в основном оставаться в кеше L3 (который является общим среди всех ядер одного процессора).
-
Каналы памяти: ваш процессор имеет 3 канала памяти. Если ваши банки памяти установлены таким образом, вы можете использовать их все (см., Например, руководство по материнской плате), вы можете запускать несколько потоков одновременно. Я видел эффекты, которые при чтении только с одним потоком асимптотическая полоса пропускания близка к одному из одного модуля памяти (например, 12,8 ГБ/с для DDR-1600), а при запуске нескольких потоков асимптотическая полоса пропускания близка к числу каналов памяти умножает полосу пропускания одного модуля памяти.
Ответ 5
Вы также можете использовать numactl, чтобы выбрать, какой node запускать процесс и где выделять память:
numactl --cpubind=0 --membind=1 <process>
Я использую это в сочетании с LMbench, чтобы получить номера латентности памяти:
numactl --cpubind=0 --membind=0 ./lat_mem_rd -t 512
numactl --cpubind=0 --membind=1 ./lat_mem_rd -t 512
Ответ 6
Если кто-то еще хочет попробовать этот тест, вот модифицированная рабочая программа. Мне бы хотелось увидеть результаты с другого оборудования. Это работает на моей машине с Linux 2.6.34-12-desktop, GCC 4.5.0, Boost 1.47.
g++ -o numatest -pthread -lboost_thread -lnuma -O0 numatest.cpp
numatest.cpp
#include <numa.h>
#include <iostream>
#include <boost/thread/thread.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <pthread.h>
void pin_to_core(size_t core)
{
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
}
std::ostream& operator<<(std::ostream& os, const bitmask& bm)
{
for(size_t i=0;i<bm.size;++i)
{
os << numa_bitmask_isbitset(&bm, i);
}
return os;
}
void* thread1(void** x, size_t core, size_t N, size_t M)
{
pin_to_core(core);
void* y = numa_alloc_local(N);
boost::posix_time::ptime t1 = boost::posix_time::microsec_clock::universal_time();
char c;
for (size_t i(0);i<M;++i)
for(size_t j(0);j<N;++j)
{
*(((char*)y) + ((j * 1009) % N)) += 1;
}
boost::posix_time::ptime t2 = boost::posix_time::microsec_clock::universal_time();
std::cout << "Elapsed read/write by same thread that allocated on core " << core << ": " << (t2 - t1) << std::endl;
*x = y;
}
void thread2(void* x, size_t core, size_t N, size_t M)
{
pin_to_core(core);
boost::posix_time::ptime t1 = boost::posix_time::microsec_clock::universal_time();
char c;
for (size_t i(0);i<M;++i)
for(size_t j(0);j<N;++j)
{
*(((char*)x) + ((j * 1009) % N)) += 1;
}
boost::posix_time::ptime t2 = boost::posix_time::microsec_clock::universal_time();
std::cout << "Elapsed read/write by thread on core " << core << ": " << (t2 - t1) << std::endl;
}
int main(int argc, const char **argv)
{
int numcpus = numa_num_task_cpus();
std::cout << "numa_available() " << numa_available() << std::endl;
numa_set_localalloc();
bitmask* bm = numa_bitmask_alloc(numcpus);
for (int i=0;i<=numa_max_node();++i)
{
numa_node_to_cpus(i, bm);
std::cout << "numa node " << i << " " << *bm << " " << numa_node_size(i, 0) << std::endl;
}
numa_bitmask_free(bm);
void* x;
size_t N(10000000);
size_t M(5);
boost::thread t1(boost::bind(&thread1, &x, 0, N, M));
t1.join();
for (size_t i(0);i<numcpus;++i)
{
boost::thread t2(boost::bind(&thread2, x, i, N, M));
t2.join();
}
numa_free(x, N);
return 0;
}