Измерение NUMA (неравномерный доступ к памяти).Нет наблюдаемой асимметрии.Зачем? - PullRequest
33 голосов
/ 31 августа 2011

Я пытался измерить эффекты асимметричного доступа к памяти в NUMA, но потерпел неудачу.

Эксперимент

Выполнен на процессоре Intel Xeon X5570 @ 2,93 ГГц, 2 процессора, 8 ядер.

В потоке, прикрепленном к ядру 0, я выделяю массив x размером 10 000 000 байтов на узле NUMA ядра 0 с numa_alloc_local.Затем я перебираю массив x 50 раз и читаю и записываю каждый байт в массив.Измерьте прошедшее время, чтобы выполнить 50 итераций.

Затем на каждом из остальных ядер моего сервера я закрепляю новый поток и снова измеряю затраченное время, чтобы выполнить 50 итераций чтения и записи для каждого байта в массиве x .

Массив x большой, чтобы минимизировать эффекты кэширования.Мы хотим измерить скорость, когда ЦП должен полностью загружать и хранить ОЗУ, а не когда кеши помогают.

На моем сервере два узла NUMA, поэтому я ожидаю, что ядраиметь сходство на том же узле, в котором выделен массив x , чтобы иметь более высокую скорость чтения / записи.Я этого не вижу.

Почему?

Возможно, NUMA имеет отношение только к системам с> 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 МБ x недостаточно велик для устранения кеш-эффектов.Я пробовал массив размером 100 Мбайт x , и я пытался выдать забор полной памяти с помощью __sync_synchronize () внутри моих внутренних циклов.По-прежнему не обнаруживается асимметрия между узлами NUMA.

Обновление 2:

Я пытался читать и записывать в массив x с__sync_fetch_and_add ().Все еще ничего.

Ответы [ 6 ]

19 голосов
/ 31 августа 2011

Первое, на что я хочу обратить внимание, это то, что вы можете перепроверить, какие ядра находятся на каждом узле.Я не помню, чтобы ядра и узлы чередовались таким образом.Кроме того, вы должны иметь 16 потоков из-за HT.(если вы его не отключили)

Еще одна вещь:

Машины Xeon с сокетом 1366 - это всего лишь немного NUMA.Поэтому будет трудно увидеть разницу.Эффект NUMA гораздо более заметен в 4P Opteron.

В таких системах, как ваша, пропускная способность между узлами на самом деле быстрее, чем пропускная способность ЦП в памяти.Поскольку ваш шаблон доступа является полностью последовательным, вы получаете полную пропускную способность независимо от того, являются ли данные локальными.Лучше измерить задержку.Попробуйте произвольный доступ к блоку размером 1 ГБ вместо последовательной потоковой передачи.

Последнее:

В зависимости от того, насколько агрессивно оптимизируется ваш компилятор, ваш цикл может быть оптимизирован, поскольку он ничего не делает:

c = ((char*)x)[j];
((char*)x)[j] = c;

Что-то подобное гарантирует, что это не будет устранено компилятором:

((char*)x)[j] += 1;
15 голосов
/ 31 августа 2011

Ах, ха! Мистик прав! Каким-то образом аппаратная предварительная выборка оптимизирует мои операции чтения / записи.

Если бы это была оптимизация кеша, то форсирование барьера памяти победило бы оптимизацию:

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

По крайней мере, я думаю, что это происходит.

Спасибо, Мистик!

РЕДАКТИРОВАТЬ: ВЫВОД ~ 133%

Для тех, кто только заглядывает в этот пост, чтобы получить приблизительное представление о характеристиках производительности NUMA, вот итог по моим тестам:

Доступ к памяти на нелокальном узле NUMA примерно в 1,33 раза больше времени доступа к памяти на локальный узел.

10 голосов
/ 23 октября 2012

Спасибо за этот контрольный код.Я взял вашу «исправленную» версию и изменил ее на чистый 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

Если у кого-то есть дальнейшие улучшения, я был бы радслышать о них.Например, это, очевидно, не идеальные измерения полосы пропускания в реальных единицах измерения (вероятно, из-за - мы надеемся, постоянного - целочисленного коэффициента).

5 голосов
/ 14 декабря 2012

несколько комментариев:

  • Чтобы увидеть структуру NUMA вашей системы (в linux), вы можете получить графический обзор, используя утилиту lstopo из hwloc библиотека.В частности, вы увидите, какие номера ядер являются членами какого узла NUMA (сокета процессора)
  • char, вероятно, не идеальный тип данных для измерения максимальной пропускной способности ОЗУ.Я подозреваю, что используя 32-битный или 64-битный тип данных, вы можете получить больше данных за то же количество циклов ЦП.
  • В целом, вам также следует проверить, что ваши измерения не ограничены скоростью процессора, а скоростью ОЗУ.Например, утилита 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 Эффекты кэширования: Полезно измерить пропускную способность в Гбайт / с в зависимости от размера блока.Вы должны увидеть примерно четыре разных скорости при увеличении размера блока, соответствующего тому, откуда вы читаете данные (кеш или основная память).В вашем процессоре, по-видимому, имеется 8 МБ кэша уровня 3 (?), Поэтому ваши 10 миллионов байтов могут в основном оставаться в кэше L3 (который используется всеми ядрами одного процессора).

  • Каналы памяти : Ваш процессор имеет 3 канала памяти .Если ваши банки памяти установлены таким образом, что вы можете использовать их все (см., Например, руководство по материнской плате), вы можете запускать более одного потока одновременно.Я видел эффекты, что при чтении только с одним потоком асимптотическая пропускная способность близка к пропускной способности одного модуля памяти (например, 12,8 ГБ / с для DDR-1600), в то время как при запуске нескольких потоков асимптотическая пропускная способность близка к числуканалов памяти, умноженных на пропускную способность одного модуля памяти.

3 голосов
/ 18 апреля 2013

Вы также можете использовать numactl, чтобы выбрать, на каком узле запускать процесс и где выделить память:

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
3 голосов
/ 01 сентября 2011

Если кто-то еще хочет попробовать этот тест, вот модифицированная, работающая программа. Я хотел бы видеть результаты от другого оборудования. Это работает на моей машине с 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;
}
...