Я выполнил немало параллелизма на уровне потоков и на уровне процессов, и теперь я пытаюсь перейти на параллелизм на уровне команд с помощью компилятора Intel C ++, что является довольно сложной задачей.
Выполняя некоторую автоматическую векторизацию циклов и анализируя логи компилятора, я нашел некоторую «Оценку максимального количества отключений цикла», которую я не могу понять.
Пример:
double a[100],x[100],y[100]
...
for (i=0; i< 100; i++) {
a[i] = x[i] + y[i];
}
Этот цикл выводит оценку максимального числа поездок за 12 поездок.
Я где-то читал, что процесс векторизации может обрабатывать в общей сложности 8 элементов за поездку, поскольку стоимость процесса каждого цикла меньше, чем 6 операций, как я могу сказать, этот примерный цикл имеет стоимость 1 магазин, 2 чтения и 1 арифметическая операция.
Таким образом, теоретически мой счет должен составлять 100/8 = 12,5 поездок, следовательно, 13 поездок.
Это сводка, сделанная компилятором? Или в фоновом режиме происходит какая-то другая оптимизация, которая позволяет процессу совершать менее 13 поездок?
Еще один вопрос, правильны ли мои предположения о 6 операциях на цикл? Есть ли случаи, когда это не относится?
заранее спасибо
Вместо того, чтобы понимать, как Intel реализует каждый цикл, давайте попробуем ответить на ваш вопрос о параллелизме на уровне команд.
Ваши операции ограничены чтением и записью, поэтому вы можете игнорировать арифметику при определении количества циклов. Вот что может делать Core2 через Broadwell:
Core2: two 16 byte reads one 16 byte write per 2 clock cycles -> 24 bytes/clock cycle
SB/IB: two 32 byte reads and one 32 byte write per 2 clock cycles -> 48 bytes/clock cycle
HSW/BDW: two 32 byte reads and one 32 byte write per clock cycle -> 96 bytes/clock cycle
Общее количество прочитанных и записанных байтов sizeof(double)*100*3=2400
, Таким образом, быстрая оценка времени, которое потребуется
Core2: 2400/24 = 100 clock cycles
SB/IB: 2400/48 = 50 clock cycles
HSW/BDW: 2400/96 = 25 clock cycles
Теперь вопрос заключается в том, как реализовать это для полной пропускной способности.
Для Core2 через Ivy Bridge одна из нагрузок может быть объединена с одной из добавок, что обойдется в одну микроплавкую микрооперацию. Другая загрузка стоит одну микрооперацию, а загрузка — одну микрооперацию. Если вы хотите делать это каждую итерацию, вы нужно уменьшить указатель и сделать условный переход, а также. Начиная с Nehalem, они могут слиться с макрокомандой, поэтому общее число операций с микроплавлением / слиянием макрокоманд на одну итерацию составляет:
Core2 Nehalem through Broadwell
vector add + load 1 1
vector load 1 1
vector store 1 1
scalar add 1 ½
conditional jump 1 ½
--------------------------------------------
total 5 4
Для Core2 через Ivy Bridge либо обеим нагрузкам нужен один и тот же порт, либо загрузке и хранилищу нужен один и тот же порт. Это требует двух тактов. Для Haswell / Broadwell это возможно благодаря каждому такту. Тем не мение, из-за ограничений порта 7 это может быть достигнуто только статически распределенными массивами используя абсолютный 32-битный адрес + смещение адресации (что случайно не возможно на OSX). Так что для Haswell / Broadwell, если массив не является статически распределенным, вы должны либо развернуть цикл, чтобы выполнить свою операцию каждый такт, либо для этого потребуется 1,5 такта на итерацию. Вот сводка тактов на одну итерацию для каждого процессора:
Core2: 5 fused micro-ops/every two clock cycles
SB/IB: 4 fused micro-ops/every two clock cycles
HSW/BDW: 4 fused mirco-ops/every clock cycle for statically allocated array
HSW/BDW: 4 fused mirco-ops/every 1.5 clock cycles for non-statically allocated arrays
Если вы использовали массивы, выделенные из стека, вы, вероятно, можете спокойно читать за пределами буфера. В противном случае вы должны заполнить ваши массивы до ширины SIMD. Количество итераций цикла тогда равно:
SSE2: (100+1)/2 = 51
AVX: (100+3)/4 = 26
По моему опыту, компилятор Intel развертывается дважды, так что это будет вдвое меньше итераций. Количество итераций, развернутое в два раза:
SSE2: (100+3)/4 = 26
AVX: (100+7)/8 = 13
Наконец, с точки зрения тактов это
Core2: 51*2 = 102 clock cycles
SB/IB: 26*2 = 51 clock cycles
HSW/BDW: 26*1.5 = 39 clock cycles for non-statically allocated arrays no-unroll
HSW/BDW: 26*1 = 26 clock cycles for statically allocated arrays no-unroll
HSW/BDW: 26*1 = 26 clock cycles with full unrolling
6-й мопс звучит как правильная оценка, если не делать разворачивания. Компилятор Intel, как правило, хорошо справляется с развертыванием автоматически векторизованных циклов, если это необходимо. В этом конкретном случае даже полная развертка может иметь смысл.
Не уверен, что у вас есть 8 элементов одновременно, потому что даже с AVX вы можете получить только 4 значения двойной точности в одном 256-битном ymm
регистр.
Что касается поездки. Даже если вы можете сделать 8 элементов одновременно, это будет 12, а не 13, потому что последние несколько элементов (которые не могут быть обработаны 8 одновременно) будут выполняться с помощью скалярного кода.
Так что с точки зрения компилятора это будет выглядеть так:
int i=0;
for(; i<(100 & ~7); i+=8) // 12 iterations
// Do vector code
for(;i<100; ++i)
// Process loop remainder using scalar code