Параллельная структура и избежание ложного обмена
Недавно я ответил на вопрос об оптимизации вероятного параллелизуемого метода для генерации каждой перестановки произвольных базовых чисел. Я опубликовал ответ, подобный списку Параллельный, плохой реализации, и кто-то почти сразу указал на это:
Это в значительной степени гарантированно дает вам ложный доступ и, вероятно, будет во много раз медленнее. (кредит gjvdkamp)
и они были правы, это была медленная смерть. Тем не менее, я исследовал эту тему и нашел интересные материалы и предложения для борьбы с ней. Если я правильно понимаю, когда потоки обращаются к непрерывной памяти (скажем, к массиву, который, вероятно, поддерживает этот ConcurrentStack
), вероятно, происходит ложное разделение.
Для кода ниже горизонтального правила a Bytes
:
struct Bytes {
public byte A; public byte B; public byte C; public byte D;
public byte E; public byte F; public byte G; public byte H;
}
Для моего собственного тестирования я хотел получить параллельную версию этого запуска и быть действительно быстрее, поэтому я создал простой пример, основанный на исходном коде. 6
как limits[0]
был ленивым выбором с моей стороны - мой компьютер имеет 6 ядер.
Блок с одним потоком Среднее время выполнения: 10 с0059мс
var data = new List<Bytes>();
var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 };
for (byte a = 0; a < limits[0]; a++)
for (byte b = 0; b < limits[1]; b++)
for (byte c = 0; c < limits[2]; c++)
for (byte d = 0; d < limits[3]; d++)
for (byte e = 0; e < limits[4]; e++)
for (byte f = 0; f < limits[5]; f++)
for (byte g = 0; g < limits[6]; g++)
for (byte h = 0; h < limits[7]; h++)
data.Add(new Bytes {
A = a, B = b, C = c, D = d,
E = e, F = f, G = g, H = h
});
Параллельная, плохая реализация. Среднее время выполнения: 81s729ms, ~ 8700.
var data = new ConcurrentStack<Bytes>();
var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 };
Parallel.For(0, limits[0], (a) => {
for (byte b = 0; b < limits[1]; b++)
for (byte c = 0; c < limits[2]; c++)
for (byte d = 0; d < limits[3]; d++)
for (byte e = 0; e < limits[4]; e++)
for (byte f = 0; f < limits[5]; f++)
for (byte g = 0; g < limits[6]; g++)
for (byte h = 0; h < limits[7]; h++)
data.Push(new Bytes {
A = (byte)a,B = b,C = c,D = d,
E = e,F = f,G = g,H = h
});
});
Параллельно,?? реализация Среднее время выполнения: 5s833ms, 92 утверждения
var data = new ConcurrentStack<List<Bytes>>();
var limits = new byte[] { 6, 16, 16, 16, 32, 8, 8, 8 };
Parallel.For (0, limits[0], () => new List<Bytes>(),
(a, loop, localList) => {
for (byte b = 0; b < limits[1]; b++)
for (byte c = 0; c < limits[2]; c++)
for (byte d = 0; d < limits[3]; d++)
for (byte e = 0; e < limits[4]; e++)
for (byte f = 0; f < limits[5]; f++)
for (byte g = 0; g < limits[6]; g++)
for (byte h = 0; h < limits[7]; h++)
localList.Add(new Bytes {
A = (byte)a, B = b, C = c, D = d,
E = e, F = f, G = g, H = h
});
return localList;
}, x => {
data.Push(x);
});
Я рад, что у меня была реализация, которая быстрее, чем однопоточная версия. Я ожидал, что результат приблизится к 10 с /6, или около 1,6 секунды, но это, вероятно, наивное ожидание.
Мой вопрос для параллелизированной реализации, которая на самом деле быстрее, чем однопоточная версия, есть ли дальнейшие оптимизации, которые могут быть применены к операции? Мне интересно об оптимизации, связанные с распараллеливанием, а не усовершенствования алгоритма, используемого для вычисления значений. В частности:
- Я знаю об оптимизации для хранения и заполнения как
struct
вместо byte[]
, но это не связано с распараллеливанием (или это?)
- Я знаю, что желаемое значение может быть оценено ленивым с помощью сумматора с волнообразным переносом, но так же, как оптимизация
struct
.
Ответы
Ответ 1
Во-первых, мое первоначальное предположение относительно Parallel.For()
и Parallel.ForEach()
было неправильным.
Плохая параллельная реализация, скорее всего, имеет 6 потоков, каждый из которых пытается записать один сингл CouncurrentStack()
сразу. Хорошая реализация, использующая локаторы потоков (объясняется ниже), только один раз получает доступ к общей переменной за задачу, что почти устраняет любые разногласия.
При использовании Parallel.For()
и Parallel.ForEach()
вы не можете просто вставить строку в строку for
или foreach
. Это не означает, что это не может быть слепым улучшением, но, не рассматривая проблему и не применяя ее, использование их бросает многопоточность на проблему, потому что это может ускорить ее работу.
** Parallel.For()
и Parallel.ForEach()
имеют перегрузки, которые позволяют создавать локальное состояние для Task
, которое они в конечном итоге создают, и запускать выражение до и после каждого выполнения итерации.
Если у вас есть операция, которую вы распараллеливаете с помощью Parallel.For()
или Parallel.ForEach()
, вероятно, рекомендуется использовать эту перегрузку:
public static ParallelLoopResult For<TLocal>(
int fromInclusive,
int toExclusive,
Func<TLocal> localInit,
Func<int, ParallelLoopState, TLocal, TLocal> body,
Action<TLocal> localFinally
)
Например, вызывая For()
для суммирования всех целых чисел от 1 до 100,
var total = 0;
Parallel.For(0, 101, () => 0, // <-- localInit
(i, state, localTotal) => { // <-- body
localTotal += i;
return localTotal;
}, localTotal => { <-- localFinally
Interlocked.Add(ref total, localTotal);
});
Console.WriteLine(total);
localInit
должен быть lambda, который инициализирует тип локального состояния, который передается в body
и localFinally
lambdas. Обратите внимание, что я не рекомендую выполнять суммирование с 1 по 100 с использованием распараллеливания, но просто попробуйте простой пример, чтобы сделать пример коротким.