В паралелен код на OpenMP ще има ли някаква полза за memset да се изпълнява паралелно?

Имам блокове памет, които могат да бъдат доста големи (по-големи от L2 кеша) и понякога трябва да ги настроя изцяло на нула. memset е добър в сериен код, но какво ще кажете за паралелния код? Някой има ли опит, ако извикването на memset от едновременни нишки действително ускорява нещата за големи масиви? Или дори да използвате обикновен openmp parallel for цикли?


person nat chouf    schedule 20.07.2012    source източник
comment
Малко вероятно. memset на данни извън кеша вероятно ще бъде затруднено от честотната лента на паметта.   -  person Mysticial    schedule 20.07.2012
comment
Изпълнението на memset паралелно на NUMA машина (и всички MP post-Core2 Intel системи, както и всички MP и дори някои UP AMD системи са NUMA) може да бъде вашият най-труден за разбиране защо убиец на производителността, освен ако по-късно същите нишки ще имат достъп само до тези части от масива, които те лично са нулирали.   -  person Hristo Iliev    schedule 20.07.2012
comment
Въпреки това съществува индустриалният стандарт STREAM benchmark. Вземете версията на OpenMP, компилирайте и стартирайте с различни брой нишки, за да видите сами. Обърнете внимание също, че memset() е активиран за SIMD в повечето libc реализации и вече увеличава честотната лента на паметта до своя връх.   -  person Hristo Iliev    schedule 20.07.2012


Отговори (2)


Хората в HPC обикновено казват, че една нишка обикновено не е достатъчна, за да насити една връзка към паметта, същото обикновено важи и за мрежовите връзки. Ето бърз и мръсен мемсеттер с активиран OpenMP, който написах за вас и който запълва с нули два пъти 2 GiB памет. И ето резултатите при използване на GCC 4.7 с различен брой нишки на различни архитектури (максимални стойности от няколко отчетени изпълнения):

GCC 4.7, код, компилиран с -O3 -mtune=native -fopenmp:

Intel Xeon X7350 с четири гнезда - четириядрен процесор преди Nehalem с отделен контролер на паметта и предна шина

единичен контакт

threads   1st touch      rewrite
1         1452.223 MB/s  3279.745 MB/s
2         1541.130 MB/s  3227.216 MB/s
3         1502.889 MB/s  3215.992 MB/s
4         1468.931 MB/s  3201.481 MB/s

(Първото докосване е бавно, тъй като екипът за нишки се създава от нулата и операционната система нанася физически страници във виртуалното адресно пространство, запазено от malloc(3))

Една нишка вече насища честотната лента на паметта на един CPU ‹-> NB връзка. (NB = северен мост)

1 резба на гнездо

threads   1st touch      rewrite
1         1455.603 MB/s  3273.959 MB/s
2         2824.883 MB/s  5346.416 MB/s
3         3979.515 MB/s  5301.140 MB/s
4         4128.784 MB/s  5296.082 MB/s

Необходими са две нишки, за да се насити пълната честотна лента на паметта на връзката NB ‹-> memory.

Окто-сокет Intel Xeon X7550 - 8-посочна NUMA система с 8-ядрени процесори (CMT деактивиран)

единичен контакт

threads   1st touch      rewrite
1         1469.897 MB/s  3435.087 MB/s
2         2801.953 MB/s  6527.076 MB/s
3         3805.691 MB/s  9297.412 MB/s
4         4647.067 MB/s  10816.266 MB/s
5         5159.968 MB/s  11220.991 MB/s
6         5330.690 MB/s  11227.760 MB/s

Необходими са поне 5 нишки, за да се насити честотната лента на една връзка с памет.

1 резба на гнездо

threads   1st touch      rewrite
1         1460.012 MB/s  3436.950 MB/s
2         2928.678 MB/s  6866.857 MB/s
3         4408.359 MB/s  10301.129 MB/s
4         5859.548 MB/s  13712.755 MB/s
5         7276.209 MB/s  16940.793 MB/s
6         8760.900 MB/s  20252.937 MB/s

Ширината на честотната лента се мащабира почти линейно с броя на нишките. Въз основа на наблюденията на единичен сокет може да се каже, че ще са необходими поне 40 нишки, разпределени като 5 нишки на сокет, за да се наситят всичките осем връзки на паметта.

Основният проблем на NUMA системите е политиката за памет при първо докосване - паметта се разпределя на NUMA възела, където се изпълнява нишката, която първа докосне виртуален адрес в рамките на конкретна страница. Фиксирането на нишки (обвързване към конкретни ядра на процесора) е от съществено значение за такива системи, тъй като миграцията на нишки води до отдалечен достъп, който е по-бавен. Поддържа се за pinnig в повечето изпълнения на OpenMP. GCC със своя libgomp има променливата на средата GOMP_CPU_AFFINITY, Intel има променливата на средата KMP_AFFINITY и т.н. Освен това OpenMP 4.0 въведе неутралната по отношение на доставчика концепция за места.

Редактиране: За пълнота, ето резултатите от изпълнението на кода с масив от 1 GiB на MacBook Air с Intel Core i5-2557M (двуядрен процесор Sandy Bridge с HT и QPI). Компилаторът е GCC 4.2.1 (компилация на Apple LLVM)

threads   1st touch      rewrite
1         2257.699 MB/s  7659.678 MB/s
2         3282.500 MB/s  8157.528 MB/s
3         4109.371 MB/s  8157.335 MB/s
4         4591.780 MB/s  8141.439 MB/s

Защо тази висока скорост дори с една нишка? Малко изследване с gdb показва, че memset(buf, 0, len) се превежда от компилатора на OS X в bzero(buf, len) и че векторизирана версия с активиран SSE4.2 с името bzero$VARIANT$sse42 се предоставя от libc.dylib и се използва по време на изпълнение. Той използва инструкцията MOVDQA за нулиране на 16 байта памет наведнъж. Ето защо дори и с една нишка честотната лента на паметта е почти наситена. Версия с активиран AVX с една нишка, използваща VMOVDQA, може да нулира 32 байта наведнъж и вероятно да насити връзката на паметта.

Важното послание тук е, че понякога векторизацията и многонишковостта не са ортогонални за ускоряване на операцията.

person Hristo Iliev    schedule 20.07.2012
comment
Благодаря ви за тези резултати. Как контролирате 1 нишка/гнездо или всички нишки в 1 гнездо? - person nat chouf; 21.07.2012
comment

Нека да разгледаме един прост пример:

Ако дефинираме:

import numpy as np
N = 5
A = np.arange(N)
X = np.arange(N*N).reshape(N,N)

B = np.arange(N)
W = np.arange(N*N).reshape(N,N)

G = np.arange(N)
Zij = np.arange(N)

Тогава първата сума, Sum_v(Av * Xi_v), може да бъде изчислена с np.dot:

In [54]: X
Out[54]: 
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24]])

In [55]: A
Out[55]: array([0, 1, 2, 3, 4])

In [56]: np.dot(X, A)
Out[56]: array([ 30,  80, 130, 180, 230])

По същия начин, втората сума, Sum_v(Bv * Wj_v) може да се изчисли като:

In [58]: np.dot(W,B)
Out[58]: array([ 30,  80, 130, 180, 230])

Ние обаче искаме първата сума да доведе до вектор, вариращ по i-индекса, докато искаме втората сума да доведе до вектор, вариращ по j-индекса. За да подредите това в numpy, използвайте излъчване:

In [59]: np.dot(X,A) + np.dot(W,B)[:,None]
Out[59]: 
array([[ 60, 110, 160, 210, 260],
       [110, 160, 210, 260, 310],
       [160, 210, 260, 310, 360],
       [210, 260, 310, 360, 410],
       [260, 310, 360, 410, 460]])

Третата сума е просто точково произведение между два едномерни масива:

In [60]: np.dot(Zij, G)
Out[60]: 30

И като съберем всичко заедно,

In [61]: M = np.dot(X,A) + np.dot(W,B)[:,None] + np.dot(Zij, G)

In [62]: M
Out[62]: 
array([[ 90, 140, 190, 240, 290],
       [140, 190, 240, 290, 340],
       [190, 240, 290, 340, 390],
       [240, 290, 340, 390, 440],
       [290, 340, 390, 440, 490]])

Забележете, че може да съм разбрал погрешно значението на Zij. Въпреки че казвате, че това е едномерен масив, може би сте имали предвид, че за всеки i,j това е едномерен масив. Тогава Z ще бъде триизмерно.

За по-голяма конкретност, нека кажем, че първите две оси на Z представляват i и j-индексите, а последната ос на Z е тази, която искате да сумирате.

В този случай бихте искали последният член да бъде np.dot(Z, G):

In [13]: Z = np.arange(N**3).reshape(N,N,-1)

In [14]: np.dot(X,A) + np.dot(W,B)[:,None] + np.dot(Z, G)
Out[14]: 
array([[  90,  190,  290,  390,  490],
       [ 390,  490,  590,  690,  790],
       [ 690,  790,  890,  990, 1090],
       [ 990, 1090, 1190, 1290, 1390],
       [1290, 1390, 1490, 1590, 1690]])
- person Hristo Iliev; 21.07.2012
comment
Това е страхотен отговор. Но бихте ли обяснили повече защо има такава разлика между първо докосване и пренаписване? Не разбирам напълно какво имате предвид под първото докосване е бавно, тъй като екипът на нишката се създава от нулата и операционната система картографира физическите страници във виртуалното адресно пространство, запазено от malloc(3) - person Z boson; 23.08.2014
comment
@Zboson, при първото извикване на malloc паметта се разпределя с помощта на анонимен mmap. Това води до картографиране във виртуалното адресно пространство на процеса, но това картографиране все още не е подкрепено от физически рамки RAM, а по-скоро специална страница на ядрото с всички нули се картографира за копиране при запис навсякъде в региона. Следователно четенето от прясно mmap-ed памет връща нули. При първо писане на някакъв адрес в този регион възниква грешка в страницата, манипулаторът на грешки намира свободен RAM кадър и го картографира към съответната страница. - person Hristo Iliev; 23.08.2014
comment
Човек може да намали излишните разходи при първото докосване, като или поиска използването на огромни страници, или като инструктира mmap(2) да предостави предварително повредена памет (на Linux от MAP_POPULATE; OS X не поддържа предварителна грешка). Във втория случай извикването на mmap ще бъде изключително бавно, но няма да има разлика в достъпа до паметта между първото докосване и презаписването. - person Hristo Iliev; 23.08.2014
comment
@HristoIliev, благодаря, така има повече смисъл. Не знаех за mmap. Имам още много да уча. - person Z boson; 23.08.2014
comment
Виждам нещо много странно. Когато обвържа нишките и задам броя на нишките на броя на физическите ядра, докосването е по-бързо! Например на моята 4-ядрена система Ivy Bridge система (8 HT) с Linux и GCC правя export OMP_NUM_THREADS=4 и export OMP_PROC_BIND=true. Получавам Touch: 21191.013 MB/s Rewrite: 18112.064 MB/s. Но без обвързване и използване на осем нишки получавам Touch: 11830.490 MB/s Rewrite: 17933.885 MB/s. Това няма смисъл за мен. - person Z boson; 25.08.2014
comment
stackoverflow.com/questions/25483363/ - person Z boson; 25.08.2014

Е, винаги има L3 кеш...

Въпреки това е много вероятно това вече да бъде обвързано с честотната лента на основната памет; добавянето на повече паралелизъм е малко вероятно да подобри нещата.

person Oliver Charlesworth    schedule 20.07.2012