Имам блокове памет, които могат да бъдат доста големи (по-големи от L2 кеша) и понякога трябва да ги настроя изцяло на нула. memset е добър в сериен код, но какво ще кажете за паралелния код? Някой има ли опит, ако извикването на memset от едновременни нишки действително ускорява нещата за големи масиви? Или дори да използвате обикновен openmp parallel for цикли?
В паралелен код на OpenMP ще има ли някаква полза за memset да се изпълнява паралелно?
Отговори (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 байта наведнъж и вероятно да насити връзката на паметта.
Важното послание тук е, че понякога векторизацията и многонишковостта не са ортогонални за ускоряване на операцията.
Нека да разгледаме един прост пример:
Ако дефинираме:
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
malloc
паметта се разпределя с помощта на анонимен mmap
. Това води до картографиране във виртуалното адресно пространство на процеса, но това картографиране все още не е подкрепено от физически рамки RAM, а по-скоро специална страница на ядрото с всички нули се картографира за копиране при запис навсякъде в региона. Следователно четенето от прясно mmap-ed памет връща нули. При първо писане на някакъв адрес в този регион възниква грешка в страницата, манипулаторът на грешки намира свободен RAM кадър и го картографира към съответната страница.
- person Hristo Iliev; 23.08.2014
mmap(2)
да предостави предварително повредена памет (на Linux от MAP_POPULATE
; OS X не поддържа предварителна грешка). Във втория случай извикването на mmap
ще бъде изключително бавно, но няма да има разлика в достъпа до паметта между първото докосване и презаписването.
- person Hristo Iliev; 23.08.2014
mmap
. Имам още много да уча.
- person Z boson; 23.08.2014
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
Е, винаги има L3 кеш...
Въпреки това е много вероятно това вече да бъде обвързано с честотната лента на основната памет; добавянето на повече паралелизъм е малко вероятно да подобри нещата.
memset
на данни извън кеша вероятно ще бъде затруднено от честотната лента на паметта. - person Mysticial   schedule 20.07.2012memset
паралелно на NUMA машина (и всички MP post-Core2 Intel системи, както и всички MP и дори някои UP AMD системи са NUMA) може да бъде вашият най-труден за разбиране защо убиец на производителността, освен ако по-късно същите нишки ще имат достъп само до тези части от масива, които те лично са нулирали. - person Hristo Iliev   schedule 20.07.2012memset()
е активиран за SIMD в повечетоlibc
реализации и вече увеличава честотната лента на паметта до своя връх. - person Hristo Iliev   schedule 20.07.2012